воскресенье, 20 марта 2011 г.

Команды в MVVM

Сами по себе команды в WPF достаточно неуклюжие, хотя и помогают избавиться от дублировании в некоторых ситуациях. Вот чудесная статья о WPF командах. В условиях MVVM представление ничего не знать о VM(ViewModel), что не дает писать code-behind для реализации реакции на события пользовательского интерфейса. То есть напрямую мы не можем привязать некоторое событие (например, нажатие кнопки) с методом класса. На помощь приходит библиотека Prism 4. Вообще Prism предназначена для написания сложных многомодульных программ в рамках MVVM. Итак для решения проблемы с командами нам понадобяться классы DelegateCommand и DelegateCommand<T> - оба наследники интерфейса ICommand. Отличаются они тем, что при активации команды DelegateCommand<T> передается аргумент типа T, в DelegateCommand аргументы не передаются. Вот сигнатуры конструкторов DelegateCommand
 public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod);  
и DelegateCommand<T>
 public DelegateCommand(Action executeMethod<T>, Func<bool,T> canExecuteMethod);  
Как несложно догадаться, первый аргумент "executeMethod" - метод выполняемы при вызове команды; второй аргумент "canExecuteMethod" возвращает true, если в данный момент разрешено активизировать команду и false, если нельзя вызвать команду. Если условия на активизацию команды не требуется можно опустить аргумент "canExecuteMethod". Надо отметить, что "canExecuteMethod" не накладывает строгий запрет на активизацию команды, то есть даже если "canExecuteMethod" вернул false мы все равно можем активизировать команду вручную. Теперь о том как привязать команду в xaml разметке. Пусть в DataContext контрола находится объект со свойством
 public DelegateCommand SomeCommand{ get; }  
тогда привязать команду к кнопке можно вот так
 <Button Command="{Binding SomeCommand}" .../>  
Значение свойства "IsEnabled" полученной кнопки будет браться из метода "canExecuteMethod". Правда есть маленький нюанс. Свойство "IsEnabled" не следит за изменением "canExecuteMethod", поэтому чтобы изменение дошло до "IsEnabled" необходимо у экземпляра DelegateCommand вызвать метод "RaiseCanExecuteChanged()". Маленький пример работы с DelegateCommand в MVVM на примере простенького TODO листа. Сделаем модель для TODO-записи
 class TodoItem  
 {  
      public bool IsCompleted { get; set; }  
      public String Title { get; set; }  
      public String Description { get; set; }  
      public int Priority { get; set; }  
      public TodoItem()  
      {  
           IsCompleted = false;  
           Title = @"Untitled";  
           Priority = 0;  
      }  
 }  
VM для этой модели будет предельно простой
 class TodoItemPresenter : INotifyPropertyChanged  
 {  
      TodoItem _item;  
      public String Title  
      {  
           get  
           {  
                return _item.Title;  
           }  
           set  
           {  
                _item.Title = value;  
                OnPropertyChanged("Title");  
           }  
      }  
      public String Description  
      {  
           get  
           {  
                return _item.Description;  
           }  
           set  
           {  
                _item.Description = value;  
                OnPropertyChanged("Description");  
           }  
      }  
      public int Priority  
      {  
           get  
           {  
                return _item.Priority;  
           }  
           set  
           {  
                _item.Priority = value;  
                OnPropertyChanged("Priority");  
           }  
      }  
      public bool IsCompleted  
      {  
           get  
           {  
                return _item.IsCompleted;  
           }  
           set  
           {  
                _item.IsCompleted = value;  
                OnPropertyChanged("IsCompleted");  
           }  
      }  
      public TodoItemPresenter(TodoItem item = null)  
      {  
           if (item == null)  
           {  
                item = new TodoItem();  
           }  
           _item = item;  
      }  
      #region INotifyPropertyChanged Members  
      public event PropertyChangedEventHandler PropertyChanged;  
      protected void OnPropertyChanged(string propertyName)  
      {  
           if (PropertyChanged != null)  
           {  
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));  
           }  
      }  
      #endregion  
 }  
Осталось написать представление для TODO-записи. Например такое.
 <DataTemplate DataType="{x:Type local:TodoItemPresenter}">  
      <Grid>  
           <Grid.ColumnDefinitions>  
                <ColumnDefinition Width="Auto"/>  
                <ColumnDefinition Width="*"/>  
                <ColumnDefinition Width="Auto"/>  
           </Grid.ColumnDefinitions>  
           <Grid.RowDefinitions>  
                <RowDefinition Height="Auto"/>  
                <RowDefinition Height="*"/>  
           </Grid.RowDefinitions>  
           <TextBlock Text="Title:" Margin="3" VerticalAlignment="Center"/>  
           <TextBox Grid.Column="1" MinWidth="100" VerticalAlignment="Center" Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}"/>  
           <ComboBox Grid.Column="2" Margin="3" SelectedIndex="{Binding Priority}">  
                <ComboBoxItem Content="None"/>  
                <ComboBoxItem Content="Law"/>  
                <ComboBoxItem Content="Medium"/>  
                <ComboBoxItem Content="Hight"/>  
                <ComboBoxItem Content="Super hight"/>  
           </ComboBox>  
           <TextBox Grid.Row="1" Grid.ColumnSpan="3" Text="{Binding Description}"  
                      TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" VerticalScrollBarVisibility="Auto"/>  
      </Grid>  
 </DataTemplate>  
Перейдем теперь к списку записей. Моделью для него будет какой-нибудь наследник интерфейса IList, например ObservableCollection. Перейду сразу к VM. Помиио самого списка и выбранного элемента добавим две команды: AddCommand для добавления новой TODO-записи и RemoveCommand для удаления выбранной записи. Разрешим удалять запись только если она помечена как законченная (IsCompleted=true).
 class TodoListPresenter : INotifyPropertyChanged  
 {  
      TodoItemPresenter _selectedItem = null;  
      ObservableCollection<TodoItemPresenter> _items;  
      public ObservableCollection<TodoItemPresenter> Items  
      {  
           get  
           {  
                return _items;  
           }  
      }  
      public TodoItemPresenter SelectedItem  
      {  
           get  
           {  
                return _selectedItem;  
           }  
           set  
           {  
                _selectedItem = value;  
                OnPropertyChanged("SelectedItem");  
                RemoveCommand.RaiseCanExecuteChanged();  
           }  
      }  
      public DelegateCommand AddCommand { get; protected set; }  
      public DelegateCommand RemoveCommand { get; protected set; }  
      public TodoListPresenter()  
      {  
           _items = new ObservableCollection<TodoItemPresenter>();  
           AddCommand = new DelegateCommand(  
                () =>  
                {  
                     TodoItemPresenter newItem = new TodoItemPresenter();  
                     _items.Add(newItem);  
                     SelectedItem = newItem;  
                });  
           RemoveCommand = new DelegateCommand(  
                () =>  
                {  
                     _items.Remove(SelectedItem);  
                     SelectedItem = null;  
                },  
                () => _selectedItem != null);  
      }  
      #region INotifyPropertyChanged Members  
      public event PropertyChangedEventHandler PropertyChanged;  
      protected void OnPropertyChanged(string propertyName)  
      {  
           if (PropertyChanged != null)  
           {  
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));  
           }  
      }  
      #endregion  
 }  
И xaml-разметка представления списка TODO-записей
 <Grid Margin="5">  
      <Grid.ColumnDefinitions>  
           <ColumnDefinition Width="Auto" MinWidth="100"/>  
           <ColumnDefinition Width="*"/>  
      </Grid.ColumnDefinitions>  
      <DockPanel>  
           <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Left">  
                <Button Content="+" Command="{Binding AddCommand}"/>  
                <Button Content="-" Command="{Binding RemoveCommand}"/>  
           </StackPanel>  
           <ListBox MinHeight="50" ItemsSource="{Binding Items}" DisplayMemberPath="Title" SelectedItem="{Binding SelectedItem}"/>  
      </DockPanel>  
      <ContentControl Margin="5" Grid.Column="1" Content="{Binding SelectedItem}">  
           <ContentControl.Resources>  
                <ResourceDictionary Source="TodoItemView.xaml"/>  
           </ContentControl.Resources>  
      </ContentControl>  
 </Grid>  
Получилось что-то вот такое 1 В целом программа работает, теперь можно заняться расширением функционала и дизайном. Исходники находятся здесь.

Комментариев нет:

Отправить комментарий