четверг, 6 марта 2008 г.

WPF ConfigurationDialog

Сообщу вам новость - во всех приложениях есть настройки! И их надо (вы не поверите) настраивать! Так вот об этом и пойдет сегодня речь - создание конфигурационного диалога.

Во-первых определимся с дизайном:

  1. это должно быть окно;
  2. оно должно быть красивое (не ко мне в другой блог пожалуйста);
  3. должен быть какой-то унифицированный механизм рендеринга настроек в диалоге.
1. Window

За основу я взял класс Lightbox.CustomDialog, т.к. мне очень понравилась анимация - диалог может анимировать 4 процесса:

  • свое появление
  • уход в тень главного окна
  • свое исчезновение
  • переход на передний план главного окна
В общем мне анимация понравилась, а видеокарточке на моем старом компе нет, поэтому я немного модифицировал класс, введя в него свойство IsAnimationEnabled, которое по умолчанию = false.

В дефолтном стиле данного диалога задается Layout - верхняя зона для контента, а в нижней части 2 кнопки Ок и Отмена.



Обратите внимание на свойство SharedSizeGroup у ColumnDefinitions и IsSharedSizeScope у Grid - удивительная прозорливость разработчиков WPF - они предусмотрели такую возможность, как разделяемый размер столбцов или строк в Grid'е. Задавая им одинаковый идентификатор вы объединяете их в одну группу с разделяемым размером (одним общим размером, максимальным из всех необходимых). Таким образом, если не задавать явно нашим столбцам размеры, их ширина будет определяться лишь максимальной шириной кнопки, которая в свою очередь определяется контентом (длиной строки в нашем случае). В итоге мы имеем 2 кнопки одинаковой ширины, которые синхронно изменят свой размер, при смене языка, т.к. на другом языке строки могут быть длиннее или короче. Удивительная прозорливость, и удивительное отсутствие нормальных средств для локализации.

Далее, кнопка "Отмена" - IsCancel = true - принажатии на нее DialogResult автоматически = false

В триггере ControlTemplate следим за свойством Validation.HasError, - используя технику продемонстрированную в предыдущем посте, можно пометить диалог как невалидный и тогда кнопка "Ок" станет неактивной.

2. Beautiful window.

Ладно, с базовым диалогом разобрались, теперь можно рисовать само окно. Layout его будет примерно следующий:


Цифрами на картинке я обозначил зоны диалога. 1,2,3 - зона контента, 4 - зона кнопок. Они определены в дефолтном стиле DialogBox. Наш класс ConfigDialog будет наследоваться от DialogBox, поэтому зоны 1,2,3 - это Content окна.

  1. Список настраиваемых элементов. Каждый элемент списка должен иметь картинку и короткий заголовок.
  2. Зона настроек все эти бла-бла-бла - настройки.
  3. Зона длинного заголовка текущего настраиваемого элемента.
Откуда берутся все эти данные? Они берутся из настраиваемых элементов, конечно.

3. Tunes.

Кому же поручить столь отвественную роль - быть настраиваемым элементом? Обратимся к предыдущим постам и вспомним паттерн DM-V-VM. Здесь VM является абстракцией View, вынося всю, неотносящуюся к rendering'у логику и состояние (state). Так если VM хранит state, тогда это самое логичное место, в которое можно поместить настройки!

Чтобы унифицировать общение диалога с настраиваемыми элементами я ввел интерфейс ITuned.


Который, реализуется классом TunedViewModel.



Теперь мы знаем, что откуда берется и можно писать сам ConfigDialog. В конструкторе ему мы будем передавать список настраиваемых элементов, которые он будет render'ить.



Для списка создается CollectionView через xaml-proxy CollectionViewSource, к которому привязан список. Здесь мы имеем классический пример master-detail binding, все элементы привязываются в binding к CollectionView, который синхронизирован с ListView, таким образом все элементы отображают текущий выбранный элемент в списке. По поводу CollectionView - это можно сказать встроенная в библиортеку реализация ViewModel для отображения коллекций в UI.

Далее, откуда берется контент настроек? Что мы собственно настраиваем?

Мы собственно настраиваем настройки - интерфейс ITune:


Этот интерфейс реализуют 2 класса. Первый Tune - простая реализация поддерживающая сохранение значений в очереди и отмену, либо подтверждение изменений через методы базов интерфейсов. Вторая же - блестящая реализация транзакционного менеджера ресурсов в памяти от Juwal Lowy, описанная в его статье в MSDN Volatile Resource Managers in .NET to bring transactions to the Common type. Данное решение позволяет максимально просто использовать преимущества транзакций при работе с данными в оперативной памяти.



Так вот эти самые настройки и предлагается редактировать в xaml. UI для редактирования настроек (Content ConfigDialog'а зона 2) передается в виде стиля WPF. Хранение его в виде стиля, а не например UserControl'а позволяет добиться следующих преимуществ:
  • хранится в View в ResourceDictionary и будет создан единожды при загрузке View как синглтон.
  • всю обработку событий этого UI code-behind View должен делегировать тому же VM классу, т.е. вся логика в VM.
  • в стиле можно использовать MarkupExtensions как источники данных для StaticResource.
Для нашего примера стиль может выглядеть вот так:


Если ваше представление имеет какие-либо настройки, оберните их в один из классов Tune или TransactionalTune и при загрузке View положите их в его ресурсы (StaticResources bla1, bla2), т.к. нет другого способа привязать их к UI (стиль в ресурсах).

ConfigDialog в обработчике события Loaded начинает транзакцию и вызывает у всех настроечных элементов метод BeginEdit, оповещая их таким образом, что редактирование началось:


В обработчике события Unloaded обрабатывется DialogResult и, в случае если он положителен (кнопка OK) транзакция подтверждается и у всех настраиваемых элементов, которые были изменены вызывается метод EndEdit, оповещаяя их об успешном завершении редактирования. Если же DialogResult != true, то в этом случае транзакция откатывается и у всех настраиваемых элементов, которые были изменены вызывается метод CancelEdit, оповещаяя их об отмене редактирования.

Summary

Как всем этим пользоваться? Очень просто. Унаследуйте свой VM от TunedViewModel, переопределите свойства, реализующие интерфейсы, напишите стиль для рендеринга настроек, оберните настройки в Tune или TransactionalTune, положите их в ресурсы View (можно переопределить метод ViewModel.OnViewLoaded), добавьте ваш ViewModel в список настраиваемых элементов и отдайте его ConfigDialog. И все. И еще, поскольку и Tune и TransactionalTune наследуются от ObservableObject, вы можете подписаться на событие PropertyChanged и реагирвать на него прямо в процессе редактирования.

И если вы попросите хорошего дизайнера поработать над вашим xaml, то можете получиь что-нибудь вроде этого:



Удачи.
PS: У Josh'a Smith'a есть пост на схожую тему - сохранение настроек окна. Почитайте, интересно.

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