вторник, 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
Ну вот и все, можно пользоваться.