суббота, 21 августа 2010 г.

WPF wizard control с поддержкой динамической загрузки страниц


Захотелось мне тут сделать Wizard UserControl, да так чтобы к нему можно было добавлять страницы во время инициализации. Те клиентский код будет создавать мой Wizard, инициализировать его своими страницами – читай usercontrol – и работать с ним.
Сначала я написал Wizard так, что все страницы были реализованы в том же проекте что и Wizard и привязаны статически к нему. При этом я, как и всякий православный WPF программист, строго придерживался MVVM. Я создал UserControl WizardView, который содержит стили, DataTemplates и прочее. WizardView агрегирует экземпляр WizardViewModel, который содержит список страниц. Схематически дизайн изображен на диаграмме ниже:
У класса WizardViewModel есть метод CurrentPage, который возвращает PageViewModelBase. По нажатию кнопки Next происходит смена CurrentPage на следующую в списке. Возникает дилемма – как сделать так, чтобы как только сменилась страница, те PageViewModel,  менялось и View. Те как сделать отображение PageViewModel <->PageView.
Самый простой метод, при котором не нужно писать ни строчки кода на C#, это использовать DataType DataTemplate:
  <DataTemplate DataType="{x:Type viewModel:SomePageViewModel}">
  <view:SomePageView /> 
DataTemplate> 
Прописываем этот код для каждой страницы в Wizard и – все магическим образом работает.
Но что если страницы приходят из клиентского кода, то что делать? Самое очевидное и простое решение добавить в код конструктора WizardView добавление соответствующих шаблонов к ресурсам.
Для начала напишем функцию, которая будет создавать DataTemplate нужного вида и добавлять его в Window.Resources:
///
/// Add DataType DataTemplates to the Resources.
/// It provides mapping between Vew and ViewModel.
/// Note that it must be called before the call of method InitializeComponent.
///
private void AddTemplateToResources(Type pageView, Type pageViewModel)
{
    //generate the following xaml code:
    //
    //    < pageView />
    //

    FrameworkElementFactory fef = new FrameworkElementFactory(pageView);

    DataTemplate dataTemplate = new DataTemplate();
    dataTemplate.DataType = pageViewModel;
    dataTemplate.VisualTree = fef;

    DataTemplateKey dtKey = new DataTemplateKey(pageViewModel);
    this.Resources.Add(dtKey, dataTemplate);
}

Fef - это содержимое DataTempalate, dtKey - это ключ для ResourceDictionary, так как Window.Resource это именно этот класс.
Далее напишем такой код в конструкторе:
    public partial class WizardDialog : Window
    {
        readonly WizardViewModel m_wizardViewModel;

        public WizardDialog(IEnumerable<WizardPage> wizardPages)
        {
            foreach (var page in wizardPages)
            {
                AddTemplateToResources(page.View.GetType(),
                   page.ViewModel.GetType());
            }

            InitializeComponent();

            …
        }
Важно чтобы ресурсы добавлялись до вызова InitializeComponent.