Пару лет назад уже была статья "Показ дочерних View в рамках патерна MVVM", т.к. сейчас это делаем по другому, да и вопрос тут возник на тостере... Еще раз, в рамках паттерна предполагается что ViewModel (бизнес-логика)
работает только с классами ViewModel и Model, а нам необходимо показать
окно, т.е. кроме создания ViewModel для него, нужно создать еще и View.
Как это сделать? Четвертый вариант под катом.
Пример будет максимально упрощен, но основные идеи постараюсь показать.
1. Создаем пустой WPF проект. В него добавляем класс окно вот с такой разметкой:
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:
///
/// Окно в котором показывается текущий ViewModel
///
private ChildWindow _wnd
= null;Пример будет максимально упрощен, но основные идеи постараюсь показать.
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
///
///
/// Заголовок окна
///
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>
Все, можно запускать наше приложение. Клики по кнопке на главной форме показывают дочерние окна, ну а клик на кнопке в дочернем окне закрывает дочернее окно:
Полный код примера можно скачать здесь.
Думаю в методе Show в строке с событием closed должно быть так
ОтветитьУдалитьviewModel._wnd.Closed += (sender, e) => viewModel.Closed();
Иначе будет вызываться Closed() базового класса.
Метод Closed виртуальный, он будет вызван для того класса от которого объект. Так что можно оставить как в примере.
УдалитьТоже так думал, он на практике вызывает именно из базового.
УдалитьА в потомке вы ключевое слово override написали?
УдалитьКонечно, ради интереса даже пример скачал и проверил
Удалитьhttps://cloud.mail.ru/public/4q3X/DCZSmTX9s
Этот комментарий был удален автором.
УдалитьПоправочка, вызывает не из базового класса, а из ViewModel, из которой идет вызов дочернего окна, если так и задумано, прошу прощения. Просто мне нужно было чтобы обработчик вызывался в то же vm, которой "принадлежит" закрываемое окно, так вроде логичнее.
УдалитьДа, основная идея в том, что родительский VM узнает о том, что дочерний закрыт. Как правило ему нужно для дальнейшей работы знать о том, что, например, в дочернем окне пользователь закончил выбор.
УдалитьОчень интересная статья, однако на практике часто приходится сталкиваться с приложением, которое имеет какую-либо фиксированную область (область с меню) и меняющуюся область- в котором показываются различные view. К сожалению в интернетах практически нет информации как организовать такое WPF приложение в рамках MVVM без code-behind. Было бы очень интересно посмотреть на такую статью.
ОтветитьУдалитьХотел написать отдельную статью, но до нового года судя по всему не успею. В описываемой вам ситуации все ешё проще. У вас есть ItemsControl (причем не важно, что это список, меню, TabControl). Коллекция которая в нем лежит содержит строку (если нужно картинку или еще что-то) для показа в этом ItemsControl-е и ViewModel, который через биндинг к выбранному элементу отображается в нужной вам области. Причем отображение один в один как то, что я показал в пример. Если не разберетесь, напишите, я на навогодних каникулах накидаю пример.
УдалитьСпасибо за статью! Очень помогла!
ОтветитьУдалитьЕсть один вопрос. Как сделать, что бы дочерние окно было в диалоговом режиме(нельзя перейти на родительское окно пока открыто дочернее)
Пожалуйста. Заменить Show на ShowDialog
УдалитьЭтот комментарий был удален автором.
ОтветитьУдалитьКак получить значения свойства Date из DemoViewModel в произвольный класс модели или MainViewModel. Что бы оно менялось в класс модели или MainViewModel при изменении его в DemoViewModel! Заранее спасибо!
ОтветитьУдалитьОбычно это делается через событие в DemoViewModel и подписывании всех заинтересованных на него.
УдалитьПри закрытии дочернего окна, как можно получить с него данные в родительском окне?
ОтветитьУдалитьУ вас в родительском окне есть ссылка на дочерний ViewModel (переменная child). Собственно из нее можно все и получить. Или реализовать в дочернем VM событие в котором sender будет дочерний ViewModel, в родительском в команде показа дочернего окна подписываетесь на событие и при вызове метода приводите sender к DemoViewModel. Чуть выше уже отвечал :) Может чуть менее подробно.
Удалить