вторник, 29 марта 2011 г.

StringFormat

Буквально пара слов о форматировании строк в биндингах xaml разметок. Довольно часто хочется не просто выводить сырое значение, а задавать какой-нибудь простенький формат. Классический пример, привязываемся с полю "Cost" выражением "{Binding Cost}", а на выходе хотим получить "Cost:[значение]". Понятно, что с помощью конверторов можно и не такое форматирование понаделать. Однако, специально писать конвертер не хочется, да и не надо. На этот случай мелкомягкие разработчики придумали поле StringFormat. Неплохие посты с примерами здесь, здесь, особенно ясный пример здесь и вот этот еще. Вот пара моих примеров использования StringConverter
 <StackPanel>  
      <TextBlock Text="{Binding Source={StaticResource DoubleValue}, StringFormat=0 }"/>  
      <TextBlock Text="{Binding Source={StaticResource DoubleValue}, StringFormat=0:00 }"/>  
      <TextBlock Text="{Binding Source={StaticResource DoubleValue}, StringFormat='${0:00.0}' }"/>  
      <TextBlock Text="{Binding Source={StaticResource DoubleValue}, StringFormat=${0:00.0} }"/>  
      <TextBlock Text="{Binding Source={StaticResource DoubleValue}, StringFormat=Cost: ${0:00.0} }"/>  
      <TextBlock Text="{Binding Source={StaticResource DoubleValue}, StringFormat=Cost: {0:00.0} dollars }"/>  
      <TextBlock Text="{Binding Source={StaticResource DoubleValue}, StringFormat='\{0:00.0\} \> \{0:00.000\}' }"/>  
 </StackPanel>  
результаты будут такими 211

пятница, 25 марта 2011 г.

MVVM и события интерфейса

Чудесный паттерн MVVM совершенно не говорит нам каким образом мы должны отлавливать и реагировать на некоторые действия пользователя. Например совершенно непонятно каким образом понять навел ли пользователь на некоторый регион интерфейса или как отловить событие Drag-and-Drop, не зная ничего о представлении. В WPF на эти случаи есть code-behind методы, однако они запрещены в MVVM. На помощь приходит система связывания триггеров и команд коих в интернете найдется с десяток и все друга на друга похожи, по крайней мере идеей. Остановлюсь на библиотеке System.Windows.Interactivity. Если я не ошибаюсь, поставляется вместе с покетом Microsoft Expression Blend SDK for .NET4, автоматом ставится с Expression Studio 4. Итак, первым дело нужно включить в xaml разметку пространство имен с расширениями
 xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"  
теперь к любому контролу можно прикрепить коллекцию Interaction.triggers, а в нее вложить треггеры которые будут вылавливать нужное нам событи. Название события указывается в триггере в поле EventName. Например, если в контроле нужно поймать событие "MouseEnter", разметка будет выглядеть так
 <i:Interaction.Triggers>  
      <i:EventTrigger EventName="MouseEnter">  
           ...  
      </i:EventTrigger>  
 </i:Interaction.Triggers>  
вместо "..." используем "i:InvokeCommandAction" к которой можно привязать любую команду как в предыдущем посте. Получится что-то вроде такого
 <i:Interaction.Triggers>  
      <i:EventTrigger EventName="MouseEnter">  
           <i:InvokeCommandAction Command="{Binding SomeCommand}"/>  
      </i:EventTrigger>  
 </i:Interaction.Triggers>  
В качестве примера реализуем простенькую рисовалку. ViewModel будет состоять из текущей недорисованной кривой, коллекции законценных кривых и трех команд. Вместе и инициализацией класс выглядит так
 using System.Collections.ObjectModel;  
 using System.ComponentModel;  
 using System.Windows;  
 using System.Windows.Input;  
 using System.Windows.Media;  
 using Microsoft.Practices.Prism.Commands;  
 namespace CommandBehaviorAPP  
 {  
      class ViewModel : INotifyPropertyChanged  
      {  
           PointCollection _currentCurve = null;  
           public ObservableCollection<PointCollection> CurvesCollection { get; protected set; }  
           public DelegateCommand StartDrawCommand { get; protected set; }  
           public DelegateCommand<IInputElement> SetNextPointCommand { get; protected set; }  
           public DelegateCommand EndDrawCommand { get; protected set; }  
           public ViewModel()  
           {  
                CurvesCollection = new ObservableCollection<PointCollection>();  
                SetNextPointCommand = new DelegateCommand<IInputElement>(  
                     inputElement =>  
                     {  
                          if (Mouse.LeftButton == MouseButtonState.Released)  
                          {  
                               StopDraw();  
                               return;  
                          }  
                          if (inputElement != null)  
                          {  
                               _currentCurve.Add(Mouse.GetPosition(inputElement));  
                               CurvesCollection.Remove(_currentCurve);  
                               CurvesCollection.Add(_currentCurve);  
                          }  
                     },  
                     point => _currentCurve != null);  
                StartDrawCommand = new DelegateCommand(  
                     () =>  
                     {  
                          _currentCurve = new PointCollection();  
                          SetNextPointCommand.RaiseCanExecuteChanged();  
                     });  
                EndDrawCommand = new DelegateCommand(  
                     () =>  
                     {  
                          StopDraw();  
                     });  
           }  
           void StopDraw()  
           {  
                _currentCurve = null;  
                SetNextPointCommand.RaiseCanExecuteChanged();  
           }  
           #region INotifyPropertyChanged Members  
           public event PropertyChangedEventHandler PropertyChanged;  
           protected void OnPropertyChanged(string propertyName)  
           {  
                if (PropertyChanged != null)  
                {  
                     PropertyChanged(this, new PropertyChangedEventArgs(propertyName));  
                }  
           }  
           #endregion  
      }  
 }  
Немного кривой выглядит команда "SetNextPointCommand". В качестве аргумента эта команда принимает элемент интерфейса относительно которого мы найдем текущие координаты мыши. Вторая неясность связанна с постоянным дерганием текущей кривой из коллекции
 ...  
 CurvesCollection.Remove(_currentCurve);  
 CurvesCollection.Add(_currentCurve);  
 ...  
такое поведение сделал из-за того, что PointCollection не является ObservableCollection. Чтобы не придумывать хитроумные механизмы и не усложнять программу решил сделать самым простым способом. Разметка кривой при условии, что в DataContext будет находится PointCollection выглядит так
 <DataTemplate x:Key="CurveTemplate">  
      <Path Stroke="Black" StrokeThickness="1">  
           <Path.Data>  
                <PathGeometry>  
                     <PathGeometry.Figures>  
                          <PathFigureCollection>  
                               <PathFigure IsClosed="False" StartPoint="{Binding .[0]}">  
                                    <PathFigure.Segments>  
                                         <PolyLineSegment Points="{Binding }"/>  
                                    </PathFigure.Segments>  
                               </PathFigure>  
                          </PathFigureCollection>  
                     </PathGeometry.Figures>  
                </PathGeometry>  
           </Path.Data>  
      </Path>  
 </DataTemplate>  
ну исамое главное, разметка основного контрола
 <Grid MinHeight="500" MinWidth="500" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >  
      <ItemsControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Azure" x:Name="MainCanvas"  
                          ItemsSource="{Binding CurvesCollection}" ItemTemplate="{StaticResource CurveTemplate}">  
           <i:Interaction.Triggers>  
                <i:EventTrigger EventName="PreviewMouseMove" >  
                     <i:InvokeCommandAction Command="{Binding SetNextPointCommand}" CommandParameter="{Binding ElementName=MainCanvas}"/>  
                </i:EventTrigger>  
                <i:EventTrigger EventName="PreviewMouseDown">  
                     <i:InvokeCommandAction Command="{Binding StartDrawCommand}"/>  
                </i:EventTrigger>  
                <i:EventTrigger EventName="PreviewMouseUp">  
                     <i:InvokeCommandAction Command="{Binding EndDrawCommand}"/>  
                </i:EventTrigger>  
                <i:EventTrigger EventName="MouseLeave">  
                     <i:InvokeCommandAction Command="{Binding EndDrawCommand}"/>  
                </i:EventTrigger>  
           </i:Interaction.Triggers>  
           <ItemsControl.ItemsPanel>  
                <ItemsPanelTemplate>  
                     <Canvas/>  
                </ItemsPanelTemplate>  
           </ItemsControl.ItemsPanel>  
      </ItemsControl>  
      <ContentControl Content="{Binding CurrentCurve}" ContentTemplate="{StaticResource CurveTemplate}"/>  
 </Grid>  
исходники находятся здесь

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

StaticResource или DynamycResource

Огромное количество форумов по WPF наскидку вспоминаются этот и этот говорят о ровно следующее. Привязка через StaticResource берет объект ровно один раз из коллекции ресурсов. DynamicResource берет объект из коллекции ресурсов каждый раз когда он потребуется. Кроме того можно подменить объект на который ссылаемся на новый и привязка DynamicResource поймет, что объект изменился и ресурс будет затребован по-новой. Вроде бы ничего сложного. Очевидно, что StaticResource требует меньше затрат, чем DynamicResoure, поэтому без особой надобности DynamicResource использовать не стоит. Наглядно демонстрирует различие между StaticResource и DynamiResource простой пример. Сделаем простую разметку с code-behind. Вот разметка
 <Window x:Class="StaticAndDynamicResources.MainWindow"  
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
           MouseLeftButtonUp="Window_MouseLeftButtonUp"  
           >  
      <Window.Resources>  
           <ResourceDictionary>  
                <SolidColorBrush Color="Yellow" x:Key="FGColor"/>  
           </ResourceDictionary>  
      </Window.Resources>  
   <Grid>  
           <Grid.ColumnDefinitions>  
                <ColumnDefinition Width="*"/>  
                <ColumnDefinition Width="*"/>  
           </Grid.ColumnDefinitions>  
           <GroupBox Header="StaticResource">  
                <Canvas Background="{StaticResource FGColor}"/>  
           </GroupBox>  
           <GroupBox Grid.Column="1" Header="DynamicResource">  
                <Canvas Background="{DynamicResource FGColor}"/>  
           </GroupBox>  
      </Grid>  
 </Window>  
вот code-behind
 public partial class MainWindow  
 {  
      public MainWindow()  
      {  
           InitializeComponent();  
      }  
      private void Window_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)  
      {  
           Resources[@"FGColor"] = new SolidColorBrush(Colors.Green);  
      }  
 }  
По клику мышки подменяется цвет кисти с желтого на зеленый. На интерфейсе все выглядит примерно так 3 Несложные исходники можно найти здесь. Теперь коснемся одной особенности StaticResource. Дело в том, что StaticResource вовсе не статичный, его можно изменять. Главное помнить,что StaticResource привязывается к одному объекту и его подменить не получится, но вот изменения в состоянии самого объекта будут влиять на значение поля к корому привязываем объект. На примере проще. Создадим класс SomeResource вот такой
 class SomeResource : INotifyPropertyChanged  
 {  
      int _nClicks = 0;  
      public int NClicks  
      {  
           get  
           {  
                return _nClicks;  
           }  
           protected set  
           {  
                _nClicks = value;  
                OnPropertyChanged("NClicks");  
           }  
      }  
      public ICommand IncCommand { get; protected set; }  
      public SomeResource()  
      {  
           IncCommand = new DelegateCommand(() => NClicks++);  
      }  
      #region INotifyPropertyChanged Members  
      public event PropertyChangedEventHandler PropertyChanged;  
      protected void OnPropertyChanged(string propertyName)  
      {  
           if (PropertyChanged != null)  
           {  
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));  
           }  
      }  
      #endregion  
 }  
и xaml разметку
 <Window x:Class="StaticFeaturesApp.MainWindow"  
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
           xmlns:local="clr-namespace:StaticFeaturesApp"  
           SizeToContent="WidthAndHeight"  
     >  
      <Window.Resources>  
           <ResourceDictionary>  
                <local:SomeResource x:Key="SomeResourceOfApp"/>  
           </ResourceDictionary>  
      </Window.Resources>  
   <Grid>  
           <Grid.RowDefinitions>  
                <RowDefinition Height="Auto"/>  
                <RowDefinition Height="Auto"/>  
           </Grid.RowDefinitions>  
           <Grid.ColumnDefinitions>  
                <ColumnDefinition Width="Auto"/>  
                <ColumnDefinition Width="Auto"/>  
           </Grid.ColumnDefinitions>  
           <TextBlock HorizontalAlignment="Right" Text="StaticResource " Margin="5"/>  
           <TextBlock Grid.Column="1" HorizontalAlignment="Left" Margin="5"   
                       Text="{Binding NClicks, Source={StaticResource SomeResourceOfApp}}"/>  
           <Button Grid.Row="2" Grid.ColumnSpan="2" HorizontalAlignment="Center" Margin="5" Padding="5,2,5,2"   
                     Content="Inc" Command="{Binding IncCommand, Source={StaticResource SomeResourceOfApp}}"/>  
      </Grid>  
 </Window>  
Этим чудесным свойством StaticResource иногда получается устроить биндинг в биндинге. Однако, полноценного связывания внутри другого связывания не получится, есть риск получить нестабильное поведение интерфейса. По нажатию на кнопку число NClicks будет увеличиваться и это отобразится на интерфейсе. Думаю, очевидно почему это так. Исходники здесь.

Команды в 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 В целом программа работает, теперь можно заняться расширением функционала и дизайном. Исходники находятся здесь.

пятница, 18 марта 2011 г.

Различие SelectedValue и SelectedItem

В WPF у всех наследниках Selector, вроде ComboBox или LixtBox есть поля SelectedValue и SelectedItem, которые часто принимают за идентичные, однако это не совсем так. SelectedItem - это выбранный объект в том виде какой он находится в коллекции. SelectedValue - это объект полученный из SelectedItem по пути указанному в поле SelectedValuePath. Простой пример:
Возмем следующий класс:
 class Number  
 {  
      public int N { get; set; }  
      public String Name { get; set; }  
      public override string ToString()
      {
           return String.Format("{0} is {1}", N, Name);
      }
      public Number(int n, String name)  
      {  
           N = n;  
           Name = name;  
      }  
 }  
Создадим окно, в качестве DataContext подсунем коллекцию из экземплярв класса Number. Конструктор окна получился такой
 public MainWindow()  
 {  
      InitializeComponent();  
      DataContext = new Number[]{new Number(1, @"one"),  
                                 new Number(2, @"two"),   
                                 new Number(3, @"three"),   
                                 new Number(4, @"four"),   
                                 new Number(5, @"five")};  
 }  
А вот сама разметка, для интереса добавил отображение полей SelectedValue и SelectedItem.
 <StackPanel>  
      <ListBox ItemsSource="{Binding}" SelectedValuePath="N" x:Name="list" Padding="2"/>  
      <StackPanel Margin="3" Orientation="Horizontal">  
           <Label Content="SelectedValue"/>  
           <TextBox Text="{Binding SelectedValue, UpdateSourceTrigger=PropertyChanged, ElementName=list}"/>  
      </StackPanel>  
      <StackPanel Margin="3" Orientation="Horizontal">  
           <Label Content=" SelectedItem"/>  
           <Label Content="{Binding SelectedItem, ElementName=list}"/>  
      </StackPanel>  
 </StackPanel>  
Окошко выглядит так
1 SelectedItem возвращает выбранный объект(типа Number), а SelectedValue возвращает значение SelectedValue.N (типа int). Кроме того, каждый объект в ListBox ассоциируется со значением своего поля N. Задав значение SelectedValue будет выбран объект у которого свойство N равно SelectedValue, если таких объектов окажется несколько, в качестве SelectedItem будет установлен первый по порядку. Исходники здесь.

четверг, 17 марта 2011 г.

Неожиданная проблема при использовании лямбда-выражений и шаблонов

В программе понадобилось создать несколько классов, позволяющих выбирать некоторый объект из коллекции. Так как все эти классы были как две капли воды похожи друг на друга, решил вытащить общий абстрактный класс примерно такой :
public abstract class SelectorBase<T>
{
public List<T> RegiteredObjects { get; private set; }
public virtual T SelectedObject { get; set; }
}

Затем по ходу развития программы некоторые методы выдавали исключительные ситуации ConnectionException, связанные с конектом к базе данных. Опять же, чтобы избежать многократного дублирования кода вытащил обработку исключительной ситуации в класс Selector.
public abstract class SelectorBase<T>
{
public bool HasProblem { get; protected set; }
public List<T> RegiteredObjects { get; private set; }
public virtual T SelectedObject { get; set; }
protected void NoExceptedAction(Action action)
{
try
{
action();
HasProblem = false;
}
catch(ConnectionException)
{
HasProblem = true;
}
}
public SelectorBase()
{
HasProblem = false;
RegiteredObjects = new List<T>();
}
}

Теперь для реализации простенького селектора, достаточно его просто наследовать от класса SelectorBase. Получается весьма лаконичный селектор, например такой
public class StringSelector : Selector<string>
{
public override string SelectedObject
{
get
{
return base.SelectedObject;
}
set
{
NoExceptedAction(
() =>
{
//здесь проверяем объекта value в базе данных
base.SelectedObject = value;
});
}
}
}
Все кажется достаточно безопасным, однако все селекторы оказались нерабочими даже в следующей маленькой программе
public class Program
{
public static void Main(string[] args)
{
var pv = new StringSelector();
pv.SelectedObject = "some string";
}
}

Программа вылетает с нечеловеческими матюгами, выдавая следующий Exception
System.BadImageFormatException 
was caught Message=Была сделана попытка загрузить программу, имеющую неверный формат. (Exception from HRESULT: 0x8007000B)
Source=MainConsole
StackTrace:
at MainConsole.StringSelector.<>c__DisplayClass1.<set_SelectedObject>b__0()
at MainConsole.Selector`1.NoExceptedAction(Action action) in <путь к файлу>


После получасовых поисков и капитального взрыва мозга удалось понять, что я напоролся на известную проблему компилятора C#. Чтобы мои селекторы заработали, пришлось немного изменить код
public class StringSelector : Selector<string>
{
public override string SelectedObject
{
get
{
return base.SelectedObject;
}
set
{
string res = null;
NoExceptedAction(
() =>
{
//здесь, например, проверяем объекта value в базе данных
//и наконец
res = value;
});
base.SelectedObject = res;
}
}
}

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

Лябда-выражения

Лябда-выражения являются реализацией замыканий. Это своего рода контейнер с функцией, который можно передавать в любую часть программы и вызвать с нужными аргументами. Кроме того лямбда-выражение связывает функцыю внутри себя с лексическим окружением. То есть лямбда-выражение видит все переменные и методы объекта в котором оно формируется.
Синтаксис лямбда-выражений очень простой состоит из дувух частей
 ([аргументы]) => {[тело функции]}
аргументы можно указывать с типами, но в большинстве случаев это не требуется. Следующая запись будет означать, что создаем лямбда-выражение с двумя аргументами num типа int и name типа string.
 (int num, string name) => {[тело функции]}
Если аргументов нет, тогда запись будет выглядеть так
 () => {[тело функции]}
Если аргумент один и не требуется указывать его тип, то можно опустить круглые скобки сократив запи
 arg => {[тело функции]}
Внутри блока {[тело функции]} можно вставлять любые операции дозволенные в текущем лексическом окружении. Если [тело функции] состоит из одной операции, то можно опустить фигурные
 ([аргументы]) => SomeFunction(...)
В этом случае лямбда-выражение вернет (если требуется) последнее значение со стека. В предыдущей записи это будет результат работы функции SomeFunction(...). А вот такая запись лямбда-выражения вернет а
 (int num, string name) => num
Ну вот и все, можно пользоваться.

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

Коротко о связываниях (binding) в WPF

Байдинги или привязки в WPF очень обширная тема, очень много статей в интернете рзаного размера побольше и поменьше, иногда попадаются удобные шпоргалки. Тем не менее на освоение этой простой технологии ушло без малого пара месяцев, хотя все очень просто.


Основные учасники привязки

Байндинги WPF связываю два объекта: Источник(Source) и Подписчик(Target). Все взаимодействие между source и target объектами укладывается в элементарную схему

На уровне xaml разметки подписчиком всегда будет контрол к которому прикручивается binding, а истоником будет оставшийся объект. Например:

<TextBlock Text="{Binding ElementName=Person}"/>

Здесь TextBlock является подписчиком, а источником будет некий элемент с именем Person.
Источником  может быть любой объект, главное правильно и корректно сосласться на него. Чаще всего приходится пользоваться такими конструкциями:
  • {Binding} В этом случае источником будет взят у контрола из поля DataContext
  • {Binding ElementName=Person} Привязываемся к контролу из xaml разметки с именем Person
  • {Binding RelativeSource={RelativeSource...}} Такая привязка используется, когда хотят привязаться к объекту по относительной связи. Подробнее здесь и здесь.
Чаще всего приходится привязываться не ко всему объекту, а к его свойствам. Для этого существует поле Path. Используется так же как и в коде. Например у нас есть класс

class Citizen
{
public string Name { get; set; }
public string Surname { get; set; }
public string Patronymic { get; set; }
}

Экземпляр лежит в DataContext контрола и мы хотим отобразить свойство Name, binding будет таким


<TextBlock Text="{Binding Path=Name}"/>

А если хотим вывести на экран длину имени, то это делается вот так

<TextBlock Text="{Binding Path=Name.Length}"/>


Направление привязки

В зависимости от ситуации может оказаться, что binding должен работать не в обе стороны, а в какую-то одну, за это поведение отвечает поле Mode:

<TextBlock Text="{Binding ElementName=Person, Mode=Default}"/>

Mode принемает одно из пяти значений:
  • Mode=TwoWay. Все изменения подписчика будут отправленны источнику и, наоборот, все изменения источника повлияют на подписчика. 

  • Mode=OneWay. Данные будут только браться из источника, но изменения подписчика никак не будут влиять на источник.


  • Mode=OneWayToSource. Данные будут браться от подписчика и устанавливаться источнику.  Будут игнорироваться изменения источника.

  • Mode=OneTime. Значение будет считано один раз из источника, дальнейшие изменения источника и подписчика игнорируются.



  • Mode=Default. Устанавливает в Mode значение по умолчанию, одно из четырех: TwoWay, OneWay, OneWayToSource или OneTime. Надо отметить, что для различных контролов в WPF значение поумолчанию разное.

Зачем нужен INotifyPropertyChanged

После всего вышенаписанного осталось непонятно, как источник сообщает о том, что он  изменился. Для этого достаточно отнаследовать класс модели от интерфейса INotifyPropertyChanged. Класс Citizen будет выглядеть примерно так


class Citizen : INotifyPropertyChanged
{
string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged("Name");
}
}

//Surname и Patronymic будут переписаны аналогично Name
public string Surname { ... }
public string Patronymic { ... }

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

Теперь когда мы изменим поле Name будет вызвано событие о том, что в этом объекте поле Name изменилось. По этому сообщению binding поймет в какой момент надо будет обновить интерфейс.


Когда изменения от подписчика дойдут до источника

С тем как подписчик узнает об изменении источника разобрались. Теперь посмотрим обратную задачу. Когда подписчик сообщает источнику о внесенных изменениях. За это в привязке отвечает поле UpdateSourceTrigger, который может принемать ондно из четырех значений.

  • UpdateSourceTrigger=PropertyChanged. Источник получит новое значение сразу же после изменения подписчика. Далеко не всегда удобный вариант. Например, если мы изменяем текст, то источнику будет присваиваться новое значение всякий раз когда мы вводим/удаляем символ. 
  • UpdateSourceTrigger=LostFocus. Все изменения передадутся источнику после того, как подписчик потеряет фокус. 
  • UpdateSourceTrigger=Default. В этом случае нужно будет лезть в документацию и смотреть какое значение ставиться. Совершенно бесполезное значение, почти всегда устанавливается как UpdateSourceTrigger=PropertyChanged
  • UpdateSourceTrigger=Explicit. Источник узнает об изменении подписчика только после вызова метода UpdateSource. Для этого необходимо иметь binding не в xaml разметке, а непосредственно в коде. подробнее рассказывать нехочу, так как используется редко. Здесь написано подробнее.


Конверторы в привязке

Совершенно прозрачна работа привязок с простыми типами данных (int, double, string). Однако этого недостаточно. Например, мы работаем с валютными операциями. По какой-то причине храним все в рублях, но пользователю нужно выдавать сумму в долларах. Привязки в WPF позволяют конвертировать сырые значения из модели в требуемые для представления. Для этого достаточно написать класс-наследник интерфейса IValueConverter. А потом подключить его в xaml разметке. Интерфейс IValueConverter требует обязательного наличия в классе двух методов.


object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)


Отвечает за преобразование значения из источника в нужный подписчику вид. И второй

object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)


Отвечает за обратное преобразование, значение от подписчику переводит в корректный для источника формат. На картинке это выглядит так


Для примера, впростой реализации конвертер валюты из рублей в доллары будет выглядеть так

class RublesToDollarsConverter : IValueConverter

{
//такой курс долллара был на момент написания поста
const double RublesPerDollar = 28.1717;

#region IValueConverter Members

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is double)
{
return (double)value / RublesPerDollar;
}
return value;
}

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is double)
{
return (double)value * RublesPerDollar;
}
return value;
}

#endregion
}


Подключить конвертер можно несколькими способами. Самый простой путь: добавить конвертер в список ресурсов xaml разметки контрола и задать этому ресурсу имя, например "ManeyConverter". Теперь там где мы хотим пользоваться конвертером в binding добавляем поле Converter={StaticResource ManeyConverter}. Привязка будет выглядеть примерно так:


{Binding ... Converter={StaticResource ResourceKey=}}.

Вот совершенно чудесная статья по конверторам.


Проверка корректности данных


Тут я ничего писать не буду, ибо в этой статье все прекрасно и коротко описано.

Ложка дегтя

Неcмотря на всю прелесть связываний, есть очень существенное ограничение. При формировании связывания нельзя ссылаться на динамические объекты. То есть внутри биндинга нельзя пользоваться еще одним биндингом или DynamicResource. Однако, это ограничение частично можно обойти через StaticResource, но об этом потом.

пятница, 4 марта 2011 г.

Вкратце об MVVM

О паттерне MVVM написано достаточно много статей тут и там, а еще здесь. Я постараюсь кратко на простом примере пояснить суть паттерна.

Чем же MVVM особенный?

MVVM очередной вариант классического паттерна MVC, создавался компанией Microsoft специально для WPF.  Про отличия MVC, MVP и MVVM можно почитать в этой статье. В MVVM приложение делится на три части: Model, View, View-Model.

Модель (Model) содержит в себе всю логику приложение, при чем модель ничего не знает о том как ее будут использовать. Здесь не должно быть ничего лишнего, особенно это касается событий вроде "Модель изменилась - пора обновить графический интерфейс". В этом случае модель получается до ужаса простой, так как программисту не надо заботиться о дизайнере и наворачивать ненужные конструкции. Более того, модель проще тестировать.

VM (View-Model) прослойка, перегоняющая функционал модели в удобный для представления вид. А так же VM должна оповещать представление о том, что модель изменилась.

Представление (View) - обычный GUI. Тут надо отметить, что представление ничего не знает ни о модели, ни о VM. То есть, представление никак не связано с VM напрямую. То есть получается следующая связь:

Такое возможно только при наличии дусторонних связок, которые бы неявным образом синхронизировали поля у представления и VM. В WPF вкачестве таких связок будут байндинги (binding).

Давайте теперь разберем патерн на простеньком примере.

Надуманный пример. 

Сделаем список заметок, который можно редактировать. Добавлять и удалять записи НЕЛЬЗЯ, для этого нужны коммады, о них я расскажу в одном из следующих постов. У заметки будет название (Title) и описание (Description). Итак идем попорядку


Модель


class NoteModel
{
public String Title { get; set; }
public String Description { get; set; }

public NoteModel(string title = "untitled", string description = null)
{
Title = title;
Description = description;
}
}


View-Model


Все VM являются наследниками INotifyPropertyChanged. Этот интерфейс необходит для оповещения презентера обо всех изменениях в моделе.


class NotePresenter : INotifyPropertyChanged
{
NoteModel _model;

public string Title
{
get
{
return _model.Title;
}
set
{
_model.Title = value;
OnPropertyChanged("Title");
}
}

public string Description
{
get
{
return _model.Description;
}
set
{
_model.Description = value;
OnPropertyChanged("Description");
}
}

public NotePresenter(NoteModel model)
{
if (model == null) throw new ArgumentNullException("model");
_model = model;
}

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string fieldName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(fieldName));
}
}

#endregion
}


Презентер

Особой красоты наводить не буду


<Window x:Class="Testing_MVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Testing MVVM" Height="350" Width="525"
>
    <Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
Grid.ColumnDefinitions>
<ListBox Grid.Column="0" ItemsSource="{Binding Notes}" x:Name="ListOfNotes">
<ListBox.ItemTemplate>
<DataTemplate>
<Border CornerRadius="5" BorderBrush="Blue" Margin="5">
<StackPanel>
<TextBlock Text="{Binding Title}"/>
<TextBlock Text="{Binding Description}"/>
StackPanel>
Border>
DataTemplate>
ListBox.ItemTemplate>
ListBox>
<ContentControl Grid.Column="1" Content="{Binding ElementName=ListOfNotes, Path=SelectedValue}"
HorizontalAlignment="Center" VerticalAlignment="Center">
<ContentControl.ContentTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" MinWidth="100"/>
Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Title" 
  VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="1" 
Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Description" 
  VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Grid.Column="1" 
Text="{Binding Description, UpdateSourceTrigger=PropertyChanged}"/>
Grid>
DataTemplate>
ContentControl.ContentTemplate>
ContentControl>
Grid>
Window>

И гланое .cs  файл привязанный к разметке


public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();

//по хорошему презентер должен создаваться и устанавливаться в другом месте
//или передаваться в конструктор
DataContext = new NotesListPresenter(
new NoteModel("Утро", "Еле встал"),
new NoteModel("Обед", "отлично поел"),
new NoteModel("Вечер", "Смотрел футбол"),
new NoteModel("Ночь", "Сладко-сладко поспал"));
}
}

Скриншот


В левой части мы выбираем запись, в правой редактируем, при этом поля изменяются одновременно.


Итог


В разметке xaml нет упонимания ни о моделе, ни о VM, а  главную VM мы подсовываем интерфейсу в качестве DataContext. Главное не запутаться в DataContext-ах. В целом мы добились нужной связи.
Исходники лежат здесь.