воскресенье, 27 декабря 2015 г.

Показ дочерних View в рамках патерна MVVM (часть 2)

Пару лет назад уже была статья "Показ дочерних View в рамках патерна MVVM", т.к. сейчас это делаем по другому, да и вопрос тут возник на тостере... Еще раз, в рамках паттерна предполагается что ViewModel (бизнес-логика) работает только с классами ViewModel и Model, а нам необходимо показать окно, т.е. кроме создания ViewModel для него, нужно создать еще и View. Как это сделать? Четвертый вариант под катом.

Пример будет максимально упрощен, но основные идеи постараюсь показать.
1. Создаем пустой WPF проект. В него добавляем класс окно вот с такой разметкой:

<Window x:Class="ChildWindowsDemo.ChildWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ChildWindowsDemo"
        mc:Ignorable="d"
        Title="{Binding Title}" SizeToContent="WidthAndHeight">
    <Grid>
        <ContentPresenter Content="{Binding }" />
    </Grid>
</Window>

В коде у него ничего не добавляем. Именно в этом окне будут показываться все дочерние ViewModel. Его состояние можно привязать к модели, например, здесь показано как привязать заголовок, но точно так же можно Visability или другие свойства (для свойств типа Visability можно через конвертор, а в можели хранить bool).
2. Добавляем класс базового ViewModel:

public class ViewModelBase : DependencyObject
{
    ///
    /// Окно в котором показывается текущий ViewModel
    ///
    private ChildWindow _wnd = null;

    ///
    /// Заголовок окна
    ///
    public string Title
    {
        get { return (string)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Title.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty TitleProperty =
        DependencyProperty.Register("Title", typeof(string), typeof(ViewModelBase), new PropertyMetadata(""));       

    ///
    /// Методы вызываемый окном при закрытии
    ///
    protected virtual void Closed()
    {

    }

    ///
    /// Методы вызываемый для закрытия окна связанного с ViewModel
    ///
    public bool Close()
    {
        var result = false;
        if (_wnd != null)
        {
            _wnd.Close();
            _wnd = null;
            result = true;               
        }
        return result;
    }

    ///
    /// Метод показа ViewModel в окне
    ///
    /// viewModel">
    protected void Show(ViewModelBase viewModel)
    {
        viewModel._wnd = new ChildWindow();
        viewModel._wnd.DataContext = viewModel;
        viewModel._wnd.Closed += (sender, e) => Closed();
        viewModel._wnd.Show();
    }
}
Потомки этого класса могут передавать произвольного потомка этого класса в метод Show, чтобы показать его в отдельном окне.
3. Создаем View для демо, т.к. у нас всегда показывается в окне из пункта 1, то View делаем на основе UserControl:

<UserControl x:Class="ChildWindowsDemo.View.DemoView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:ChildWindowsDemo.View"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <StackPanel Width="200">
        <DatePicker SelectedDate="{Binding Date}" />
        <Button Command="{Binding CloseCommand}">Закрыть</Button>
    </StackPanel>
</UserControl>
Наш дочерний ViewModel позволяет вводить дату и содержит кнопку для закрытия окна.
4. Демонстрационный ViewModel, потомок нашего ViewModelBase:

class DemoViewModel : ViewModelBase
{
    public DateTime Date
    {
        get { return (DateTime)GetValue(DateProperty); }
        set { SetValue(DateProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Date.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DateProperty =
        DependencyProperty.Register("Date", typeof(DateTime), typeof(DemoViewModel), new PropertyMetadata(null));

    public ICommand CloseCommand
    {
        get { return (ICommand)GetValue(CloseCommandProperty); }
        set { SetValue(CloseCommandProperty, value); }
    }

    // Using a DependencyProperty as the backing store for CloseCommand.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CloseCommandProperty =
        DependencyProperty.Register("CloseCommand", typeof(ICommand), typeof(DemoViewModel), new PropertyMetadata(null));

    public DemoViewModel()
    {
        CloseCommand = new SimpleCommand(() => Close());
    }
}

5. Главный ViewModel тоже является потомком BaseViewModel, в нем реализуем показ дочернего окна вызовом метода Show:

class MainViewModel : ViewModelBase
{
    public ICommand CreateChildCommand { get; set; }

    public MainViewModel()
    {
        CreateChildCommand = new SimpleCommand(CreateChild);
    }

    private void CreateChild()
    {
        var child = new DemoViewModel()
        {
            Title = "Дочернее окно",
            Date = DateTime.Now
        };
        Show(child);
    }
}

6. Ну и магия, в ресурсах приложения создаем связку между View и ViewModel:
<Application.Resources>
    <DataTemplate DataType="{x:Type viewmodel:DemoViewModel}">
        <view:DemoView HorizontalAlignment="Stretch" />
    </DataTemplate>
</Application.Resources>

Все, можно запускать наше приложение. Клики по кнопке на главной форме показывают дочерние окна, ну а клик на кнопке в дочернем окне закрывает дочернее окно:
Полный код примера можно скачать здесь.

17 комментариев:

  1. Думаю в методе Show в строке с событием closed должно быть так
    viewModel._wnd.Closed += (sender, e) => viewModel.Closed();
    Иначе будет вызываться Closed() базового класса.

    ОтветитьУдалить
    Ответы
    1. Метод Closed виртуальный, он будет вызван для того класса от которого объект. Так что можно оставить как в примере.

      Удалить
    2. Тоже так думал, он на практике вызывает именно из базового.

      Удалить
    3. А в потомке вы ключевое слово override написали?

      Удалить
    4. Конечно, ради интереса даже пример скачал и проверил
      https://cloud.mail.ru/public/4q3X/DCZSmTX9s

      Удалить
    5. Этот комментарий был удален автором.

      Удалить
    6. Поправочка, вызывает не из базового класса, а из ViewModel, из которой идет вызов дочернего окна, если так и задумано, прошу прощения. Просто мне нужно было чтобы обработчик вызывался в то же vm, которой "принадлежит" закрываемое окно, так вроде логичнее.

      Удалить
    7. Да, основная идея в том, что родительский VM узнает о том, что дочерний закрыт. Как правило ему нужно для дальнейшей работы знать о том, что, например, в дочернем окне пользователь закончил выбор.

      Удалить
  2. Очень интересная статья, однако на практике часто приходится сталкиваться с приложением, которое имеет какую-либо фиксированную область (область с меню) и меняющуюся область- в котором показываются различные view. К сожалению в интернетах практически нет информации как организовать такое WPF приложение в рамках MVVM без code-behind. Было бы очень интересно посмотреть на такую статью.

    ОтветитьУдалить
    Ответы
    1. Хотел написать отдельную статью, но до нового года судя по всему не успею. В описываемой вам ситуации все ешё проще. У вас есть ItemsControl (причем не важно, что это список, меню, TabControl). Коллекция которая в нем лежит содержит строку (если нужно картинку или еще что-то) для показа в этом ItemsControl-е и ViewModel, который через биндинг к выбранному элементу отображается в нужной вам области. Причем отображение один в один как то, что я показал в пример. Если не разберетесь, напишите, я на навогодних каникулах накидаю пример.

      Удалить
  3. Спасибо за статью! Очень помогла!
    Есть один вопрос. Как сделать, что бы дочерние окно было в диалоговом режиме(нельзя перейти на родительское окно пока открыто дочернее)

    ОтветитьУдалить
  4. Этот комментарий был удален автором.

    ОтветитьУдалить
  5. Как получить значения свойства Date из DemoViewModel в произвольный класс модели или MainViewModel. Что бы оно менялось в класс модели или MainViewModel при изменении его в DemoViewModel! Заранее спасибо!

    ОтветитьУдалить
    Ответы
    1. Обычно это делается через событие в DemoViewModel и подписывании всех заинтересованных на него.

      Удалить
  6. При закрытии дочернего окна, как можно получить с него данные в родительском окне?

    ОтветитьУдалить
    Ответы
    1. У вас в родительском окне есть ссылка на дочерний ViewModel (переменная child). Собственно из нее можно все и получить. Или реализовать в дочернем VM событие в котором sender будет дочерний ViewModel, в родительском в команде показа дочернего окна подписываетесь на событие и при вызове метода приводите sender к DemoViewModel. Чуть выше уже отвечал :) Может чуть менее подробно.

      Удалить