воскресенье, 8 января 2012 г.

Введение в ARC

Вступление можно и не читать.

За последние полгода разработки под apple устройства постоянно натыкаюсь на то, что мало кто из знакомых коллег понимает новую технологию работы с памятью в XCode. Мне и самому не до конца понятно как там все закручено, но все таки некоторые моменты постараюсь описать, заодно сам разберусь что к чему. Все картинки и примеры приведены для XCode 4.2.1 (4D502). Для тех кто в танке, ARC - Automatic Reference Counting. Итак поехали.



О чем речь?

ARC - встроенная в компилятор LLVM технология, которая совмещает прелести автоматического сборщика мусора GC и высокую скорость работы ручного управления памяти (retain/release). Работа с ARC напоминает работу на языке программирования со встроенным сборщиком мусора. Теперь почти не надо заботиться об утечках памяти, не нужно помнить и просчитывать когда отпускать/захватывать переменную и не надо помнить об assign свойствах. Кода становится меньше, а стабильность и скорость выполнения программы увеличивается. Все это стало возможным благодаря компилятору LLVM. На этапе предкомпиляции LLVM сам вставляет в нужных местах retain, release и autorelease. Естественно необходимо следовать некоторым правилам, чтобы ARC сработал корректно и не добавлял лишних операция с циклическими ссылками. О правилах я напишу в следующий раз. И что самое потрясающее - на надо выкидывать старый код программы. Можно постепенно внедрить в него технологию ARC, так что пользователи и заказчики вообще ничего не заметят. Резюмируя все вышесказанное:



Главные прелести ARC

  • Не надо использовать retain и release ВООБЩЕ!!!
  • ARC не создает дополнительных поток для удаления мусора
  • Легко понять принципы, если не вдаваться в детали
  • LLVM делает серьезную оптимизацию кода, после чего программа работает даже быстрее, чем с ручным управлением памяти.
  • Есть инструменты для постепенного внедрения ARC в существующую программу
  • Появились крайне полезные слабые связи для свойств. Раньше это было доступно только для переменных.
  • Меньше EXC_BAD_ACCESS вылетов. Программа работает стабильнее и быстрее
  • Другие плюшки по вкусу…


Как включить?

Xcode версии 4 и выше поумолчанию использует компилятор LLVM, который в свою очередь тянет за собой использование ARC. На картинке показано какой флаг включает ARC.

LLVM ARC Preference

Если нужно использовать ARC не для всех классов, то достаточно этому классу выставить флаг -fno-objc-arc и этот класс будет скомпилирован без ARC. Чтобы выставить флаг нажно зайти в настройки таргета во вкладку Build Phases и там конкретному файлу установить флаг, как на картинке.

Unset ARC flag


продолжение скоро будет


Источники

вторник, 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;
}
}
}