sobota, 24 października 2015

WPF - Podstawy wzorca MVVM Cz 4.- ICommand i obsługa przycisków i bindowanie

Dziś zajmiemy się obsługą przycisków wg wzorca MVVM będziemy bindować również wartości z TextBoxów. 



Jako Pierwszym najłatwiejszym przyciskiem, który obsłużymy jest Button Zamknij aplikację. W tym celu zapoznamy się z klasą RelayCommand:

public class RelayCommand : ICommand
    {
        readonly Action<object> _execute;
        readonly Predicate<object> _canExecute;

        public RelayCommand(Action<object> execute)
            : this(execute, null)
        {
        }

        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            _execute = execute;
            _canExecute = canExecute;
        }

        public bool CanExecute(object parameters)
        {
            return _canExecute == null ? true : _canExecute(parameters);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameters)
        {
            _execute(parameters);
        }
    }

Na pierwszy rzut oka może się to wydać skomplikowane, jednak warte uwagi są jedynie dwie metody Execute i CanExecute, a także konstruktor klasy. Jak widać konstruktor wymaga delegatów. Pierwszy argument ma być metodą, która nic nie zwraca i może przyjąć parametr typu object. Jak sama nazwa wskazuje w konstruktorze "_execute" - jest to metoda, która coś wykona w przypadku jak zostanie wykonana czynność czyli np naciśnięcie przycisku. Kolejny delegat to Predicate czyli metoda, która zwraca bool i może przyjąc parametr typu object przy pomocy tej metody będzie sprawdzany warunek czy metoda w delegacie _execute może w ogóle zostać wykonana. Jeśli skorzystamy z drugiego konstruktora czyli:

public RelayCommand(Action<object> execute) : this(execute, null) {}

od razu metoda _canexecute zwracać będzie wartość true czyli warunek zawsze będzie spełniony. Stwórzmy zatem nowy folder o nazwie "helperClasses"  i umieścimy sobie tam w/w klasę.


Jako pierwsze zaimplementujmy metodę wykonawczą, która będzie wyłączała program podczas naciśnięcia przycisku:

private void CloseApplication(object obj)
{
    Application.Current.Shutdown();
}

Następnie zaimplementujmy właściwość, która obsłuży przycisk. Zwróćmy uwagę na to, że xaml ma już wbudowaną obsługę interfejsu ICommand, której strukturę za chwilę przedstawię. a więc z poziomu MainWindowViewModel właściwość będzie wyglądała następująco:

private ICommand _shutdownApplicationCommand = null;

public ICommand ShutApplicationCommand
{
    get
    {
        if (_shutdownApplicationCommand == null)
        {
            _shutdownApplicationCommand = new RelayCommand(
                p => CloseApplication(null), null);
        }
        return _shutdownApplicationCommand;
    }
}

Teraz Przejdźmy do MainWindow i zaimplementujmy obsługę przycisku "Zamknij Aplikację" w xaml:

<Button Name="CloseApp" Content="Zamknij aplikację" Height="40" Margin="4" 
                    Command="{Binding ShutApplicationCommand,Source={StaticResource MainWindowViewM}}"/>

Gotowe. Zauważmy, że możemy zbindować właściwość znajdującą się w klasie MainWindowViewModel i po naciśnięciu uruchamia przypisaną do niej metodę.

Przejdźmy teraz do przycisku "Usuń Pracownika". Zaczniemy od dodania metody Remove w klasie WorkerRepository:


public void Remove(Worker worker)
{
    _studentList.Remove(_studentList.Single(x=>x.WorkerId == worker.WorkerId));
}

Tworzymy teraz kolejną klasę do obsługi nazwijmy ją ObservableObject, która dziedziczy po INotifyPropertyChanged. Wewnątrz klasy znajduje się delegat INotifyPropertyChanged.PropertyChangedEventHandler, który będzie obsługiwał zdarzenie, gdy właściwość zostanie zmieniona:

public class ObservableObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string ev)
    {
       PropertyChange(new PropertyChangedEventArgs(ev));
    }

    private void PropertyChange(PropertyChangedEventArgs ev)
    {
       if (PropertyChanged != null)
       {
           PropertyChanged(this, ev);
       }
     }
}

Przejdźmy teraz do MainWindowViewModel i stwórzmy właściwość, gdzie będzie przechowywana wartośc wybranego wiersza, dodajmy także pole ICommand, które obsłuży nasz przycisk a także 2 metody. Jedna z nich będzie sprawdzała, czy został zaznaczony któryś z wierzy do usunięcia (jeśli nie zostanie wybrany żaden z wierszy przycisk będzie nieaktywny). W drugiej metodzie będzie implementacja usunięcia wybranej wartości z listy:

public class MainWidnowViewModel : ObservableObject
    {
        //...

        public WorkerRowViewModel SelectedWorkerRow { get; set; }

        private ICommand _eraseRow = null;

        public ICommand EraseRowCommand
        {
            get
            {
                if (_eraseRow == null)
                {
                    _eraseRow = new RelayCommand(p => EraseSelectedRow(SelectedWorkerRow),
                        p=>CheckIsSelectedRow(SelectedWorkerRow));
                }
                return _eraseRow;
            }
        }

        private void EraseSelectedRow(WorkerRowViewModel value)
        {
            _repository.Remove(new Worker {WorkerId = value.WorkerId});
            OnPropertyChanged("WorkerList");
        }

        private bool CheckIsSelectedRow(WorkerRowViewModel workerRowViewModel)
        {
            if (workerRowViewModel == null)
            {
                return false;
            }
            return true;
        }
       //....
    }
}

Metoda w powyższym kodzie OnPropertyChanged("WorkerList"); Umożliwia odświeżanie DataGrid, gdy zostanie usunięty którykolwiek z rekordów. Teraz Przejdźmy do widoku okna i zaimplementujemy przekazanie wartości z rekordu DataGrid'a do wspomnianej właściwości SelectedWorkerRow w MainWindowViewModel:

<DataGrid Name="DGrStudentsTable" Grid.Column="1" Margin="5" Grid.ColumnSpan="2" 
           CanUserAddRows="False" AutoGenerateColumns="False"
           ItemsSource="{Binding Source={StaticResource MainWindowViewM}, Path=WorkerList}"
           SelectedItem="{Binding Source={StaticResource MainWindowViewM},Path=SelectedWorkerRow, Mode=TwoWay}"
          >
    <DataGrid.Columns>
        <DataGridTextColumn Header="Id" Binding="{Binding WorkerId}"/>
        <DataGridTextColumn Header="Temat" Binding="{Binding Name}"/>
        <DataGridTextColumn Header="Nazwisko" Binding="{Binding Surname}"/>
        <DataGridTextColumn Header="Satus Pracownika" Binding="{Binding WorkerStatus}"/>
    </DataGrid.Columns>
</DataGrid>
Teraz można usuwać rekordy, jednak, gdy nie wybierzemy żadnej z wierszy - przycisk nie będzie aktywny:


Przejdźmy do ostatniej sekcji tego wpisu - Przycisku - "Dodaj Pracownika". 

Aby wypełnić wartościami - najlepiej byłoby przenieść nasz słownik, który do tej pory był w właściwości WorkerList do pola w Klasie.

public class MainWidnowViewModel : ObservableObject
    {
        private Dictionary<eStatus, string> eStatusDictionary = new Dictionary<eStatus, string>
        {
            {eStatus.CodeWriter,"Programista"},
            {eStatus.TeamManager,"Zwierzchnik Drużyny"},
            {eStatus.Tester, "Tester Programów"}
        };
//...... Reszta klasy
}

Dzięki temu obsłużymy oczywiście wcześniejszą właściwość WorkerList i dodatkową właściwość dla Combobox'a:

public WorkerRowViewModel SelectedWorkerRow { get; set; }

        public List<string> WorkerStatusToString
        {
            get
            {
                return eStatusDictionary.Values.ToList();
            }
        }
}

Teraz przechodzimy do Combobox'a w MainWindow.xaml. I uzupełniamy o atrybut:

<ComboBox Name="CboxStatus" Style="{StaticResource CustomXBoxStyle}"
                      ItemsSource="{Binding Source={StaticResource MainWindowViewM},Path=WorkerStatusToString}"
                      IsSynchronizedWithCurrentItem="True"
/>

Kolejnym Etapem będzie obsługa ostatniego przycisku "Dodaj Pracownika"  Przejdźmy więc do klasy WorkerRepository i stwórzmy nową metodę dodającą nowego pracownika. Kolejne Id pracownika musi być przydzielane do każdego nowo dodanego pracownika dołóżmy zatem jeszcze prywatną metodę generującą Id:


private static int ActualId;

private int GenereateId()
{
    return ++ActualId;
}

I metoda dodająca pracownika do "Bazy Danych":

public void Add(Worker worker)
{
    worker.WorkerId = GenereateId();
    _studentList.Add(worker);
}

W klasie MainWindowViewModel, gdzie dodajemy Właściwości do obsługi TextBoxów: Imię, Nazwisko i Comboboxa - Status:

private string _nameValueText;

public string NameValueText
{
    get
    {
        return _nameValueText;
    }
    set
    {
        _nameValueText = value;
        OnPropertyChanged("NameValueText");
    }
}

private string _surnameValueText;

public string SurnameValueText
{
    get
    {
        return _surnameValueText;
    }
    set
    {
        _surnameValueText = value;
        OnPropertyChanged("SurnameValueText");
    }
}

private string _statusValueText;

public string StatusValueText
{
    get
    {
        return _statusValueText;
    }
    set
    {
        _statusValueText = value;
        OnPropertyChanged("StatusValueText");
    }
}

Teraz w MainWindow.xaml Wprowadzamy Implementację odczytu tych właściwości z poziomu kontrolek. Warto zwrócić uwagę na to, że argument Mode ustawiamy na "TwoWay" co oznacza, że dane z kontroli będą odsyłane od strony użytkownika poprzez wpis do TextBoxa jak i również z poziomu źródła (zmazanie wartości po wprowadzeniu danych).

<TextBox Name="TxtName" Height="40" Width="120" Text="{Binding Source={StaticResource MainWindowViewM},
                Path=NameValueText,Mode=TwoWay}" Style="{StaticResource CustomTextBoxStyle}"/>
<TextBox Name="TxtSurname" Height="40" Width="120" Style="{StaticResource CustomTextBoxStyle}"
                Text="{Binding Source={StaticResource ResourceKey=MainWindowViewM},Path=SurnameValueText,Mode=TwoWay}"/>
<ComboBox Name="CboxStatus" Style="{StaticResource CustomXBoxStyle}"
                      ItemsSource="{Binding Source={StaticResource MainWindowViewM},
                Path=WorkerStatusToString}" IsSynchronizedWithCurrentItem="True"
                      SelectedValue="{Binding Source={StaticResource MainWindowViewM},
                Path=StatusValueText}"
/>

W Comboboxie można wyróżnić dwa atrybuty: Itemsource oraz SelectedValue. Itemsource określa skąd mają być pobierane dane wyświetlane w comboboxie. Wartości te muszą być podane w formie listy i również muszą być przekonwertowane na typ string. Stwórzmy teraz obsługę przycisku. Jak w przypadku innych buttonów musimy zastosować właściwość typu ICommand i przypisać do typu RelayCommand dwie metody - jedna sprawdzająca czy zostały wypełnione wskazane TextBoxy druga wykonująca procedurę wprowadzania wartości z kontrolek do listy _repository.

private ICommand _eraseRow = null;

public ICommand EraseRowCommand
{
    get
    {
        if (_eraseRow == null)
        {
            _eraseRow = new RelayCommand(p => EraseSelectedRow(SelectedWorkerRow),
                p => CheckIsSelectedRow(SelectedWorkerRow));
        }
        return _eraseRow;
    }
}

private void EraseSelectedRow(WorkerRowViewModel value)
{
    _repository.Remove(new Worker { WorkerId = value.WorkerId });
    OnPropertyChanged("WorkerList");
}

private bool CheckIsSelectedRow(WorkerRowViewModel workerRowViewModel)
{
    if (workerRowViewModel == null)
    {
        return false;
    }
    return true;
}

Zadanie wykonane. Wszytkie nasze kontrolki działają. Wpisanie nowego pracownika wymaga wypełnienia Textboxów oraz wskazanie statusu w comboboxie. Usunięcie pracownika wymaga wybranie go z listy. Zgodnie z wzorcem MVVM nie powinniśmy wprowadzać implementacji w tzw. CodeBehind MainWindow.xaml.cs i rzeczywiście pliku nie zostały wprowadzane żadne zmiany.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

Oto nasz gotowy projekt:


Pod linkiem znajduje się cały projekt z 3-częściowego kursu: 





Brak komentarzy:

Prześlij komentarz