суббота, 5 сентября 2009 г.

WPF. ItemsSource для ComboBox и ListBox своими руками

Недавно работал над одним приложением с использование WPF. WPF использовался мною впервые, и по ходу разработки мне волей-неволей пришлось наизобретать кучу велосипедов. Честно говоря, я не считаю изобретение велосипедов плохой практикой. После создания своего чудо-творения, понимание "Best Practice" происходит на абсолютно ином уровне, начинаешь понимать, что действительно стоит за этими словами.

Итак, библиотека с классами, которая представляла Бизнес-Логику приложения была мне предоставлена. По сути это COM+ библиотека, содержимое которой для меня было черным ящиком и не поддавалось модификации. Что получил, то все мое. Объекты созданные из классов этой библиотеки - огромное количество данных (выраженные в свойствах), и некоторые методы, для выполнения на ними действий (грубо говоря, методы производящие вычисления над данными, чтение и запись их базу данных). Что бы было понятнее о чем идет речь, представляю очень упрощенную модель. Ею и буду пользоваться в демонстрационных примерах для изложения предлагаемой идеи:

class DataModel {   int taskID = 1;   int priorityID = 2;   public void Calc() {     // происходит вычисление ...     // данные класса могут измениться   }   public int TaskID {     get { return taskID; }     set { taskID = value; }   }   public int PriorityID {     get { return priorityID; }     set { priorityID = value; }   } } * This source code was highlighted with Source Code Highlighter.

Свойства вида propertynameID представляют собой не что иное как внешние ключи (FK, Foreign Key) к таблицам-словарям. Ну что же, раз речь идет о словарях, то и ввод пользователя должен лежать в пределах значений этих словарей. Естественно, что ввод пользователя в эти поля осуществляется при помощи таких элементов управления, которые предоставляют выбор значения из некоторого списка допустимых значений - ComboBox и ListBox. Причем наименования этих значений представлен пользователю нормальным человеческим описанием. Потому единицей значения словаря стал следующий класс Item:

public class Item {   int uniqueID;   String description;   public Item(int uniqueID, String description) {     this.uniqueID = uniqueID;     this.description = description;   }   public int UniqueID {     get { return uniqueID; }   }   public String Description {     get { return description; }   } } * This source code was highlighted with Source Code Highlighter.

Следующая абстракция - класс, который объединяет Item-объекты в одну коллекцию. После непродолжительных размышлений я решил в нем же сохранять ссылку на выбранный пользователем элемент из этой коллекции. Вот как это выглядит::

public class ItemsSource: INotifyPropertyChanged {   ObservableCollection<item> items = new ObservableCollection<item>();   Item currentItem = null;   public void Clear() {     items.Clear();     CurrentItem = null;   }   public void Add(Item item) {     items.Add(item);   }   public void SetCurrentItemByUniqueID(int uniqueID)   {     foreach (Item item in items)       if (item.UniqueID == uniqueID) {         CurrentItem = item;         return;       }     throw new ArgumentException();   }   #region INotifyPropertyChanged Member   public event PropertyChangedEventHandler PropertyChanged;   private void raisePropertyChanged(string propertyName) {     if (PropertyChanged != null)       PropertyChanged(this, new PropertyChangedEventArgs(propertyName));   }   #endregion   public ObservableCollection<item> Items {     get { return items; }   }   public Item CurrentItem {     get { return currentItem; }     set {       if (currentItem != value) {         currentItem = value;         raisePropertyChanged("CurrentItem");       }     }   } } * This source code was highlighted with Source Code Highlighter.

Пройдемся по основным элементам этого класса.
  • Поле items является коллекцией объектов Item, которая будет служить источником данных для свойства ItemsSource в ListBox и ComboBox. Коллекция ObservableCollection была выбрана не случайно, потому как она реализует INotifyPropertyChanged-интерфейс, и потому может без проблем быть использована для привязки. Кто еще не в курсе, что это такое бегом в гугл - ключевые слова "WPF, Binding". Отмечу, что по-началу использовал коллекцию List, но в ComboBox появлялись артефакты - пустое пространство внизу раскрытого списка.
  • В поле currentItem я храню выбранное значение, ссылка на один из элементов коллекции items. Если взглянуть на свойство, которое предоставляет доступ к полю CurrentItem, видим, что оно тоже может быть использовано в привязке, т.к. использует событие PropertyChanged, которое в свою очередь является частью (надо сказать основной частью) интерфейса INotifyPropertyChanged.
  • Методы Clear и Add служат для управления содержимым коллекции items, названия говорят за себя.
  • SetCurrentItemByUniqueID устанавливает текущее значение из коллекции items.

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

для начала XAML окна:

<window x:Class="ItemSourceObjekt.MainWindow"     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:local="clr-namespace:ItemSourceObjekt"     Title="MainWindow" Width="400" Height="300">   <stackpanel>     <label>ComboBox</Label>     <combobox x:Name="TaskComboBox" ItemsSource="{Binding Path=Items}"          DisplayMemberPath="Description"          SelectedItem="{Binding Path=CurrentItem}"       />     <label>ListBox</Label>     <listbox x:Name="TaskListBox" ItemsSource="{Binding Path=Items}"          DisplayMemberPath="Description"          SelectedItem="{Binding Path=CurrentItem}"          />   </StackPanel> </Window> * This source code was highlighted with Source Code Highlighter.

В этом коде нет ничего особенного. Содержимое окна представляет ComboBox и ListBox. Интереснее выглядит инициализация этих элементов. Заходя вперед скажу, что DataContext этих элементов установлен объект нашего класса ItemsSource. Ну теперь думаю все ясно. ItemSource инициализированной коллецией Items из нашего объекта. DisplayMemberPath инициализирован свойством Description объекта Item. Свойство SelectedItem привязан к элементу CurrentItem.

Код для создания источников данных (ItemsSource), а также их привязка к ComboBox и ListBox выглядит в упрощенном виде следующим образом:

public partial class MainWindow : Window {   DataModel model = new DataModel();   public MainWindow()   {     InitializeComponent();     ItemsSource taskItemsSource = new ItemsSource();     taskItemsSource.Add(new Item(1, "Помыть посуду"));     taskItemsSource.Add(new Item(2, "Вынести мусор"));     taskItemsSource.Add(new Item(3, "Подмести пол"));     taskItemsSource.Add(new Item(4, "Выгулять собаку"));     taskItemsSource.Add(new Item(5, "Принять душ"));     taskItemsSource.SetCurrentItemByUniqueID(model.TaskID);     taskItemsSource.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(taskItemSource_PropertyChanged);     TaskComboBox.DataContext = taskItemsSource;     TaskListBox.DataContext = taskItemsSource;   }   void taskItemSource_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {     int id = ((ItemsSource)sender).CurrentItem.UniqueID;     // можно записать в нашу модель     model.TaskID = id;   } } * This source code was highlighted with Source Code Highlighter.

Здесь в первых строках конструктора происходит создание экземпляра класса ItemSource, который затем заполняется определенными значениями. Важный момент - установка текущего значения, оно инициализируется значением из DataModel. Для установления момента изменения значения воспользуемся уже реализованным механизмом - событием PropertyChanged. DataContext-у наших элементов управления присваивается инициализированный ItemSource-объект. Когда пользователь выбирает очередное значение из списка, изменяется значение CurrentItem. Наш код обрабатывает это событие и извлекает выбранное значение. Здесь в упрощенном варианте это значение прямиком отправляется в DataModel.

Надеюсь, что я смог в доступной форме изложить идею. На самом деле это очень упрощенный вариант, не хотелось захламлять его. Но все же я введу еще один класс, который я использовал в своем проекте. Этот класс осуществлял доступ к базе данных и инициализировал объекты ItemsSource значениями из таблиц-словарей. В общем это выглядело примерно так:

class ItemsSourceInitializer {   public void InitTasks(ItemsSource itemsSource) {     // Здесь на самом деле должна быть инициализация значениями из базы данных     itemsSource.Add(new Item(1, "Помыть посуду"));     itemsSource.Add(new Item(2, "Вынести мусор"));     itemsSource.Add(new Item(3, "Подмести пол"));     itemsSource.Add(new Item(4, "Выгулять собаку"));     itemsSource.Add(new Item(5, "Принять душ"));   } } * This source code was highlighted with Source Code Highlighter.

Хочу еще раз отметить, что данное решение было создано на этапе освоения WPF и потому не лишено недостатков. Когда находишься в стрессе - новая технология и поджимает время, мозг порождает невероятные решения. Но время показало, что решение было относительно удачным, код получился ясным и структурированным.

3 комментария:

max_cn комментирует...

Привет. Я тоже только разбираюсь с особенностями WPF и многое мне в диковинку. Я решал похожую задачу в образовательных целях и в поиске ответов наткнулся на твой блог.
Моей целью было связать класс-коллекцию с Listbox'ом, так что бы последний обновлялся при изменении не только к-ва элементов, а и значения отображаемого поля.
После некоторых танцев с бубном получилось достаточно похоже на твой вариант, только INotifyPropertyChanged интерфейс пришлось наследовать во вложенном классе.

max_cn комментирует...

Хотел выложить пример своего кода - но сайт говорит о недопустимых тегах, могу выслать на мыло, если интересно.

Вообще, меня сильно разочаровало, как Microsoft реализовал XAML да и в самой реализации WPF много странностей :-\.
Это логично интерфейс вынести в отдельный модуль, что бы им занимались дизайнеры, но почему, например, привязку данных нельзя сделать в C# коде?
В результате многие ошибки оказываются в XAML, где их отлаживать по человечески просто невозможно :-\.

ИМХО, WPF - это сплошное блуждание программиста в потемках, которое несмотря на интересные идеи (авторасположение интерфейса, присоединенные и зависимые свойства и тп.) сводит на нет типовую безопасность кода и его удобство отладки :(.

SeLo комментирует...

Привет.
Да, несомненно, WPF и использование его совсем не вписывается в обычный императивный мир программиста. Уже не раз писано, что нужно быть готовым перевернуть свое мировозрение с ног на голову. Ничего страшного, расширишь только свой кругозор. К тому же, мой опыт показал, декларативный подход, кажущийся по-началу таким чуждым, на самом деле не вызывает особых проблем при отладке кода.

Не понял про привязку, что имеешь ввиду, в принципе все что в XAML возможно, возможно и коде.

Для публикации кода попробуй воспользоваться одним из генераторов подстведки исходного кода. Я в своем посте использовал http://source.virtser.net/default.aspx.