Время от времени, участвую в обсуждениях, как правильно в рамках паттерна MVVM показывать дополнительные (дочерние View) в отдельных окнах. Т.е. когда смотришь на диаграмму иллюстрирующую паттерн, все ясно: View знает про ViewModel только на уровне имен свойств указанных в биндинге, ViewModel вообще ничего не знает про View. Но вот, пользователь нажимает на кнопку и нам надо создать новые View и ViewModel для того, чтобы показать это все в отдельном окне... Что делать? Вот об этом сегодня и пойдет речь.
Как я уже сказал, этот вопрос вызывает массу споров, что делать, ведь знание одной части об другой нарушит всю красоту паттерна. Как практически у всех задач, у этой есть несколько решений. Давайте сейчас три из них и рассмотрим. Заранее прошу прощение, за простоту примеров, но я стремлюсь показать идею, а не сложный пример. Пусть у нас в программе есть главное окно со своим ViewModel, в котором храниться коллекция int-ов и две команды. По первой будет открываться View и ViewModel предназначенные для редактирования первого элемента коллекции (окно будет открываться в модальном режиме), по второй, будет открываться View, в который в качестве контекста будет передаваться наша коллекция и он ее будет показывать.
Первый View будет вот такой:
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"
mc:Ignorable="d"
>
<Grid>
<TextBox Text="{Binding Value}" Margin="5" Width="100" />
</Grid>
</UserControl>
Его ViewModel тоже не будет отличаться сложностью:
public int Value { get; set; }
}
Для показа, мы даже ViewModel создавать не будем, а обойдемся только View:
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"
mc:Ignorable="d" >
<ListBox ItemsSource="{Binding}" Width="100" />
</UserControl>
Разметка главной формы:
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="150" Width="300">
<StackPanel>
<Button Content="Редактировать первый элемент" Command="{Binding ShowFirstViewCommand}" />
<Button Content="Показать список" Command="{Binding ShowSecondViewCommand}" />
</StackPanel>
</Window>
ViewModel главной формы:
public ICommand ShowFirstViewCommand { get; set; }
public ICommand ShowSecondViewCommand { get; set; }
private ObservableCollection<int> _data;
public MainViewModel()
{
_data = new ObservableCollection<int>(new int[] { 1, 2, 3, 4 });
ShowFirstViewCommand = new SimpleCommand(ShowFirstView);
ShowSecondViewCommand = new SimpleCommand(ShowSecondView);
}
public void ShowFirstView()
{
}
public void ShowSecondView()
{
}
}
Все сцена готова, осталось дописать код в обработчики команд. Начнем.
1. Никто ни про кого ничего не знает
Сторонниками "чистых" паттернов этот подход признается наиболее правильным, поэтому с него и начнем. В проект добавляется специальный класс, который получает информацию о том, с каком режиме показывать новое окно, признак для выбора создаваемого View и DataContext для него и метод, который надо вызвать по закрытию окна.
Например, такой класс может иметь вид:
public static void Show(int p_viewIndex, object p_dataContext, bool p_isModal, Action<bool?> p_closeAction)
{
UserControl control = null;
switch (p_viewIndex)
{
case 0:
control = new EditView();
break;
case 1:
control = new ShowListView();
break;
default:
throw new ArgumentOutOfRangeException("p_viewIndex", "Такого индекса View не существует");
}
if (control != null)
{
Window wnd = new Window();
wnd.SizeToContent = SizeToContent.WidthAndHeight;
control.DataContext = p_dataContext;
StackPanel sp = new StackPanel();
sp.Children.Add(control);
Button applyButton = new Button();
applyButton.Content = "Принять";
applyButton.Click += (s, e) => { if (p_isModal) wnd.DialogResult = true; else wnd.Close(); };
StackPanel buttonPanel = new StackPanel();
buttonPanel.Orientation = Orientation.Horizontal;
buttonPanel.Children.Add(applyButton);
sp.Children.Add(buttonPanel);
wnd.Content = sp;
wnd.Closed += (s, e) => p_closeAction(wnd.DialogResult);
if (p_isModal)
{
Button cancelButton = new Button();
cancelButton.Content = "Отмена";
cancelButton.Click += (s, e) => wnd.DialogResult = false;
buttonPanel.Children.Add(cancelButton);
wnd.ShowDialog();
}
else
{
wnd.Show();
}
}
}
}
В этом случае обработчики команд в главном ViewModel будет иметь вид:
EditViewModel vm = new EditViewModel() { Value = _data[0] };
ViewShower.Show(0, vm, true, b => { if (b != null && b.Value) _data[0] = vm.Value; });
}
public void ShowSecondView()
{
ViewShower.Show(1, _data, false, b => { });
}
Теперь. при нажатии на первую кнопку, мы можем редактировать в модальном режиме первый элемент коллекции, при нажатии на вторую видеть список.
Запускаем приложение:
Нажимаем вторую кнопку:
Нажимаем первую кнопку: Меняем значение на 5 и нажимаем принять:
Плюсом этого подхода является чистота паттерна. А вот минусов будет чуть больше. При добавлении нового View, нам для его показа надо будет не только писать вызов метода Show, но и править сам метод Show. А это, при командной работе может оказаться чревато, если двое правят его одновременно и для очередного признака напишут вызов конструктора разных View. Мерджить будет забавно...
2. ViewModel вызывает конструктор View
Идея похожая, но теперь во View мы вызываем конструктор, а метод Show получает не признак и ViewModel, а только View:
if (p_view != null)
{
Window wnd = new Window();
wnd.SizeToContent = SizeToContent.WidthAndHeight;
StackPanel sp = new StackPanel();
sp.Children.Add(p_view);
Button applyButton = new Button();
applyButton.Content = "Принять";
applyButton.Click += (s, e) => { if (p_isModal) wnd.DialogResult = true; else wnd.Close(); };
StackPanel buttonPanel = new StackPanel();
buttonPanel.Orientation = Orientation.Horizontal;
buttonPanel.Children.Add(applyButton);
sp.Children.Add(buttonPanel);
wnd.Content = sp;
wnd.Closed += (s, e) => p_closeAction(wnd.DialogResult);
if (p_isModal)
{
Button cancelButton = new Button();
cancelButton.Content = "Отмена";
cancelButton.Click += (s, e) => wnd.DialogResult = false;
buttonPanel.Children.Add(cancelButton);
wnd.ShowDialog();
}
else
{
wnd.Show();
}
}
}
Соответственно методы обработчики команд будут иметь вид:
EditViewModel vm = new EditViewModel() { Value = _data[0] };
EditView view = new EditView() { DataContext = vm };
ViewShower.Show(view, true, b => { if (b != null && b.Value) _data[0] = vm.Value; });
}
public void ShowSecondView()
{
ShowListView view = new ShowListView() { DataContext = _data };
ViewShower.Show(view, false, b => { });
}
В этом случае чистота паттерна нарушается, но не придется при добавлении каждого нового View редактировать метод Show.
3. Конструктор View не вызываем но про View знаем
Это нечто промежуточное между первым и вторым подходом, кстати по аналогичной схеме сделан переход между страницами в Windosw Store приложениях. Здесь мы в конструктор передаем ViewModel и тип, на основе которого надо создать View:
Control view = null;
var constructor = p_viewType.GetConstructor(new Type[0]);
if (constructor != null)
{
view = constructor.Invoke(new object[0]) as UserControl;
}
if (view != null)
{
view.DataContext = p_dataContext;
Window wnd = new Window();
wnd.SizeToContent = SizeToContent.WidthAndHeight;
StackPanel sp = new StackPanel();
sp.Children.Add(view);
Button applyButton = new Button();
applyButton.Content = "Принять";
applyButton.Click += (s, e) => { if (p_isModal) wnd.DialogResult = true; else wnd.Close(); };
StackPanel buttonPanel = new StackPanel();
buttonPanel.Orientation = Orientation.Horizontal;
buttonPanel.Children.Add(applyButton);
sp.Children.Add(buttonPanel);
wnd.Content = sp;
wnd.Closed += (s, e) => p_closeAction(wnd.DialogResult);
if (p_isModal)
{
Button cancelButton = new Button();
cancelButton.Content = "Отмена";
cancelButton.Click += (s, e) => wnd.DialogResult = false;
buttonPanel.Children.Add(cancelButton);
wnd.ShowDialog();
}
else
{
wnd.Show();
}
}
}
Ну и наши методы:
EditViewModel vm = new EditViewModel() { Value = _data[0] };
ViewShower.Show(typeof(EditView), vm, true, b => { if (b != null && b.Value) _data[0] = vm.Value; });
}
public void ShowSecondView()
{
ViewShower.Show(typeof(ShowListView), _data, false, b => { });
}
Все. Какой из методов выбрать? Это зависит от вашей ситуации. Если одни и те же ViewModel планируется использовать, например, в Windows и Windows Store приложениях, то скорее всего первый, а вот если View у вас под одну платформу, то второй случай понятность структуры приложения за счет применения MVVM повышает, а вот лишних проблем с методом Show нет.
Как я уже сказал, этот вопрос вызывает массу споров, что делать, ведь знание одной части об другой нарушит всю красоту паттерна. Как практически у всех задач, у этой есть несколько решений. Давайте сейчас три из них и рассмотрим. Заранее прошу прощение, за простоту примеров, но я стремлюсь показать идею, а не сложный пример. Пусть у нас в программе есть главное окно со своим ViewModel, в котором храниться коллекция int-ов и две команды. По первой будет открываться View и ViewModel предназначенные для редактирования первого элемента коллекции (окно будет открываться в модальном режиме), по второй, будет открываться View, в который в качестве контекста будет передаваться наша коллекция и он ее будет показывать.
Первый View будет вот такой:
<UserControl x:Class="WpfApplication41.View.EditView"
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"
mc:Ignorable="d"
>
<Grid>
<TextBox Text="{Binding Value}" Margin="5" Width="100" />
</Grid>
</UserControl>
Его ViewModel тоже не будет отличаться сложностью:
class
EditViewModel
{public int Value { get; set; }
}
Для показа, мы даже ViewModel создавать не будем, а обойдемся только View:
<UserControl x:Class="WpfApplication41.View.ShowListView"
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"
mc:Ignorable="d" >
<ListBox ItemsSource="{Binding}" Width="100" />
</UserControl>
Разметка главной формы:
<Window x:Class="WpfApplication41.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="150" Width="300">
<StackPanel>
<Button Content="Редактировать первый элемент" Command="{Binding ShowFirstViewCommand}" />
<Button Content="Показать список" Command="{Binding ShowSecondViewCommand}" />
</StackPanel>
</Window>
ViewModel главной формы:
class
MainViewModel
{public ICommand ShowFirstViewCommand { get; set; }
public ICommand ShowSecondViewCommand { get; set; }
private ObservableCollection<int> _data;
public MainViewModel()
{
_data = new ObservableCollection<int>(new int[] { 1, 2, 3, 4 });
ShowFirstViewCommand = new SimpleCommand(ShowFirstView);
ShowSecondViewCommand = new SimpleCommand(ShowSecondView);
}
public void ShowFirstView()
{
}
public void ShowSecondView()
{
}
}
Все сцена готова, осталось дописать код в обработчики команд. Начнем.
1. Никто ни про кого ничего не знает
Сторонниками "чистых" паттернов этот подход признается наиболее правильным, поэтому с него и начнем. В проект добавляется специальный класс, который получает информацию о том, с каком режиме показывать новое окно, признак для выбора создаваемого View и DataContext для него и метод, который надо вызвать по закрытию окна.
Например, такой класс может иметь вид:
class
ViewShower
{public static void Show(int p_viewIndex, object p_dataContext, bool p_isModal, Action<bool?> p_closeAction)
{
UserControl control = null;
switch (p_viewIndex)
{
case 0:
control = new EditView();
break;
case 1:
control = new ShowListView();
break;
default:
throw new ArgumentOutOfRangeException("p_viewIndex", "Такого индекса View не существует");
}
if (control != null)
{
Window wnd = new Window();
wnd.SizeToContent = SizeToContent.WidthAndHeight;
control.DataContext = p_dataContext;
StackPanel sp = new StackPanel();
sp.Children.Add(control);
Button applyButton = new Button();
applyButton.Content = "Принять";
applyButton.Click += (s, e) => { if (p_isModal) wnd.DialogResult = true; else wnd.Close(); };
StackPanel buttonPanel = new StackPanel();
buttonPanel.Orientation = Orientation.Horizontal;
buttonPanel.Children.Add(applyButton);
sp.Children.Add(buttonPanel);
wnd.Content = sp;
wnd.Closed += (s, e) => p_closeAction(wnd.DialogResult);
if (p_isModal)
{
Button cancelButton = new Button();
cancelButton.Content = "Отмена";
cancelButton.Click += (s, e) => wnd.DialogResult = false;
buttonPanel.Children.Add(cancelButton);
wnd.ShowDialog();
}
else
{
wnd.Show();
}
}
}
}
В этом случае обработчики команд в главном ViewModel будет иметь вид:
public
void ShowFirstView()
{EditViewModel vm = new EditViewModel() { Value = _data[0] };
ViewShower.Show(0, vm, true, b => { if (b != null && b.Value) _data[0] = vm.Value; });
}
public void ShowSecondView()
{
ViewShower.Show(1, _data, false, b => { });
}
Теперь. при нажатии на первую кнопку, мы можем редактировать в модальном режиме первый элемент коллекции, при нажатии на вторую видеть список.
Запускаем приложение:
Нажимаем вторую кнопку:
Нажимаем первую кнопку: Меняем значение на 5 и нажимаем принять:
Плюсом этого подхода является чистота паттерна. А вот минусов будет чуть больше. При добавлении нового View, нам для его показа надо будет не только писать вызов метода Show, но и править сам метод Show. А это, при командной работе может оказаться чревато, если двое правят его одновременно и для очередного признака напишут вызов конструктора разных View. Мерджить будет забавно...
2. ViewModel вызывает конструктор View
Идея похожая, но теперь во View мы вызываем конструктор, а метод Show получает не признак и ViewModel, а только View:
public
static void
Show(Control p_view, bool p_isModal, Action<bool?> p_closeAction)
{if (p_view != null)
{
Window wnd = new Window();
wnd.SizeToContent = SizeToContent.WidthAndHeight;
StackPanel sp = new StackPanel();
sp.Children.Add(p_view);
Button applyButton = new Button();
applyButton.Content = "Принять";
applyButton.Click += (s, e) => { if (p_isModal) wnd.DialogResult = true; else wnd.Close(); };
StackPanel buttonPanel = new StackPanel();
buttonPanel.Orientation = Orientation.Horizontal;
buttonPanel.Children.Add(applyButton);
sp.Children.Add(buttonPanel);
wnd.Content = sp;
wnd.Closed += (s, e) => p_closeAction(wnd.DialogResult);
if (p_isModal)
{
Button cancelButton = new Button();
cancelButton.Content = "Отмена";
cancelButton.Click += (s, e) => wnd.DialogResult = false;
buttonPanel.Children.Add(cancelButton);
wnd.ShowDialog();
}
else
{
wnd.Show();
}
}
}
Соответственно методы обработчики команд будут иметь вид:
public
void ShowFirstView()
{EditViewModel vm = new EditViewModel() { Value = _data[0] };
EditView view = new EditView() { DataContext = vm };
ViewShower.Show(view, true, b => { if (b != null && b.Value) _data[0] = vm.Value; });
}
public void ShowSecondView()
{
ShowListView view = new ShowListView() { DataContext = _data };
ViewShower.Show(view, false, b => { });
}
В этом случае чистота паттерна нарушается, но не придется при добавлении каждого нового View редактировать метод Show.
3. Конструктор View не вызываем но про View знаем
Это нечто промежуточное между первым и вторым подходом, кстати по аналогичной схеме сделан переход между страницами в Windosw Store приложениях. Здесь мы в конструктор передаем ViewModel и тип, на основе которого надо создать View:
public
static void
Show(Type p_viewType, object p_dataContext, bool
p_isModal, Action<bool?> p_closeAction)
{Control view = null;
var constructor = p_viewType.GetConstructor(new Type[0]);
if (constructor != null)
{
view = constructor.Invoke(new object[0]) as UserControl;
}
if (view != null)
{
view.DataContext = p_dataContext;
Window wnd = new Window();
wnd.SizeToContent = SizeToContent.WidthAndHeight;
StackPanel sp = new StackPanel();
sp.Children.Add(view);
Button applyButton = new Button();
applyButton.Content = "Принять";
applyButton.Click += (s, e) => { if (p_isModal) wnd.DialogResult = true; else wnd.Close(); };
StackPanel buttonPanel = new StackPanel();
buttonPanel.Orientation = Orientation.Horizontal;
buttonPanel.Children.Add(applyButton);
sp.Children.Add(buttonPanel);
wnd.Content = sp;
wnd.Closed += (s, e) => p_closeAction(wnd.DialogResult);
if (p_isModal)
{
Button cancelButton = new Button();
cancelButton.Content = "Отмена";
cancelButton.Click += (s, e) => wnd.DialogResult = false;
buttonPanel.Children.Add(cancelButton);
wnd.ShowDialog();
}
else
{
wnd.Show();
}
}
}
Ну и наши методы:
public
void ShowFirstView()
{EditViewModel vm = new EditViewModel() { Value = _data[0] };
ViewShower.Show(typeof(EditView), vm, true, b => { if (b != null && b.Value) _data[0] = vm.Value; });
}
public void ShowSecondView()
{
ViewShower.Show(typeof(ShowListView), _data, false, b => { });
}
Все. Какой из методов выбрать? Это зависит от вашей ситуации. Если одни и те же ViewModel планируется использовать, например, в Windows и Windows Store приложениях, то скорее всего первый, а вот если View у вас под одну платформу, то второй случай понятность структуры приложения за счет применения MVVM повышает, а вот лишних проблем с методом Show нет.
Большое спасибо за замечательную статью! Она мне очень пригодилась.
ОтветитьУдалитьP. S. Вместо номера представления (p_viewIndex) я бы использовал enum. С перечислением гораздо нагляднее. Однако его также придется дополнять при добавлении в программу нового окна.
В блоге есть вторая часть про дочерние View. Там подход универсальней.
Удалить