MVVM the old way – czyli jak mi to kiedyś pokazano

Witam

W dzisiejszym wpisie o tym jak zrobić prostą aplikacje korzystając z wzorca MVVM w WPF. Sposób w jaki tutaj to zostanie napisane został mi pokazany na samym początku mojej nauki MVVM przez dobrego znajomego (za co mu jeszcze raz dziękuje).

Należy wspomnieć że sposób ten nie zawiera żadnego frameworka lub innych “wspomagaczy” ułatwiających czy przyspieszających tworzenie aplikacji okienkowych w tym wzorcu projektowym.

Będę przedstawiał to w krokach, niektóre “oczywiste” rzeczy pominę, a niektóre mniej jasne opiszę dokładniej. A więc zaczynamy:

Kroki:

0. Tworzymy nowy projekt WPF Application

1. Tworzymy model który będziemy wyświetlać w widoku naszej aplikacji. Model może być zwykłą klasa (POCO), może być też np. modelem bazy, tabeli stworzonym przy pomocy Entity Framworka. W tym przykładzie będzie to zwykła klasa. Na co trzeba zwrócić szczególną uwagę w tym kroku to że należy zaimplementować interfejs INotifyPropertyChanged, który pozwoli nam śledzić zmiany poszczególnych własności obietków.

Nasza klasa modelu wraz z implementacją interfejsu:

public class Person : INotifyPropertyChanged
    {
        string _FirstName;
        public string FirstName
        {
            get
            {
                return _FirstName;
            }
            set
            {
                if (_FirstName != value)
                {
                    _FirstName = value;
                    RaisePropertyChanged("FirstName");
                }
            }
        }

        string _LastName;
        public string LastName
        {
            get
            {
                return _LastName;
            }
            set
            {
                if (_LastName != value)
                {
                    _LastName = value;
                    RaisePropertyChanged("LastName");
                }
            }
        }

        int _Age;
        public int Age
        {
            get
            {
                return _Age;
            }
            set
            {
                if (_Age != value)
                {
                    _Age = value;
                    RaisePropertyChanged("Age");
                }
            }
        }

        #region INotifyPropertyChanged members
        void RaisePropertyChanged(string prop)
        {
            if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(prop)); }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
    }

2. Tworzymy klasę View Modelu którzy umożliwi obsługę naszego modelu na widoku.

Istotne punkty w klasie w View Modelu to że:

a. Też musi implementować INotifyPropertyChanged.
b. Kolekcje obiektów należy przechowywać najlepiej w ObservableCollection (która to domyślnie implementuje ten interfejs)

public class MainViewModel : INotifyPropertyChanged
    {
        public ObservableCollection<Person> People { get; set; }

        public MainViewModel()
        {
            People = new ObservableCollection<Person>
            {
                new Person { FirstName="Tom", LastName="Jones", Age=80 },
                new Person { FirstName="Dick", LastName="Tracey", Age=40 },
                new Person { FirstName="Harry", LastName="Hill", Age=60 },
            };

            MyCmd = new MyCommand(this);
        }

        private MyCommand _myCmd;
        public MyCommand MyCmd
        {
            get
            {
                return _myCmd;
            }
            set
            {
                _myCmd = value;
                RaisePropertyChanged("MyCmd");
            }
        }

        private Person _SelectedPerson;
        public Person SelectedPerson
        {
            get
            {
                return _SelectedPerson;
            }
            set
            {
                if (_SelectedPerson != value)
                {
                    _SelectedPerson = value;
                    RaisePropertyChanged("SelectedPerson");
                }
            }
        }

Ponadto aby wszystko nam się ładnie wyświetlało na widoku możemy po dodaniu klasy View Modelu ustawić DataContext okna na nasz view model. Można to zrobić w konstruktorze głównego okna, lub bezpośrednio w xamlu. Istnieją też inne sosoby jak np w metodzie OnStartup klasy App.

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            //Set DataContext to View Model
            DataContext = new MainViewModel();
        }
    }

3. Aby móc bindować komendy przycisków potrzebujemy jeszcze jednej klasy, która będzie implementować interfejs ICommand.

public class MyCommand : ICommand
        {
            private MainViewModel viewModel;

            public MyCommand(MainViewModel vm)
            {
                viewModel = vm;
            }

            #region CanExecute
            bool ICommand.CanExecute(object parameter)
            {
                var par = parameter as string;

                if (par == "AddUser")
                {
                    return true;
                }
                else if (par == "DelUser")
                {
                    if (viewModel.SelectedPerson == null)
                        return false;
                }
                return true;
            }
            #endregion

            #region Execute
            void ICommand.Execute(object parameter)
            {
                var par = parameter as string;

                if (par == "AddUser")
                {
                    viewModel.People.Add(new PersonModel { FirstName = "[Type Name]", LastName = "[Type Last Name]", Age = DateTime.Now.Second });
                }
                else if (par == "DelUser")
                {
                    viewModel.People.Remove(viewModel.SelectedPerson);
                }
            }
            #endregion
            #region ICommand EventHandler
            event EventHandler ICommand.CanExecuteChanged
            {
                add { CommandManager.RequerySuggested += value; }
                remove { CommandManager.RequerySuggested -= value; }
            }
            #endregion
        }

4. Po tych zabiegach pozostał nam juz tylko widok i bindowanie kontrolek oraz komend.

<Window x:Class="MVVMoldWayBlog.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        WindowStartupLocation="CenterScreen"
        SizeToContent="WidthAndHeight">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="0.939*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <GroupBox Header="Classic MVVM Example" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <StackPanel Orientation="Vertical">
                    <ScrollViewer VerticalScrollBarVisibility="Auto">
                        <StackPanel Orientation="Horizontal">

                            <!--Bindowanie kontrolek-->

                            <ListBox ItemsSource="{Binding Path=People, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding SelectedPerson}" DisplayMemberPath="FirstName" HorizontalAlignment="Left"/>
                            <DataGrid ItemsSource="{Binding Path=People, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding SelectedPerson}" HorizontalAlignment="Left" Margin="5,0,0,0"/>
                            <ComboBox ItemsSource="{Binding Path=People, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding SelectedPerson}" DisplayMemberPath="FirstName" Margin="5,0,0,5" VerticalAlignment="Top"/>
                        </StackPanel>
                    </ScrollViewer>
                    <StackPanel Orientation="Horizontal">

                        <!--Bindowanie komend-->

                        <Button Grid.Row="1" Content="Add person" Command="{Binding Path=MyCmd}" CommandParameter="AddUser" Margin="5" Focusable="False"/>
                        <Button Grid.Row="1" Content="Delete person" Command="{Binding Path=MyCmd}" CommandParameter="DelUser" Margin="5" Focusable="False"/>
                    </StackPanel>
                </StackPanel>
            </Grid>
        </GroupBox>
    </Grid>
</Window>

Źródła projektu do pobrania: MVVMoldWayBlog