Give some command to View in MVVM
Let's imagine I have some user control. The user control has some child windows. And user control user wants to close child windows of some type. There is a method in user control code behind:
public void CloseChildWindows(ChildWindowType type)
{
...
}
But I can't call this method as I don't have direct access to the view.
Another solution I think about is to somehow expose user control ViewModel as one of its properties (so I can bind it and give command directly to ViewModel). But I don't want user control users to know anything about user control ViewModel.
So what is the right way to solve this problem?
I feel I just found a rather nice MVVM solution to this problem. I wrote a behavior that is exposing a type property WindowType
and a boolean property Open
. DataBinding the latter allows the ViewModel to open and close the windows easily, without knowing anything about the View.
Gotta love behaviors... :)
Xaml:
<UserControl x:Class="WpfApplication1.OpenCloseWindowDemo"
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:WpfApplication1"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<local:ViewModel />
</UserControl.DataContext>
<i:Interaction.Behaviors>
<!-- TwoWay binding is necessary, otherwise after user closed a window directly, it cannot be opened again -->
<local:OpenCloseWindowBehavior WindowType="local:BlackWindow" Open="{Binding BlackOpen, Mode=TwoWay}" />
<local:OpenCloseWindowBehavior WindowType="local:YellowWindow" Open="{Binding YellowOpen, Mode=TwoWay}" />
<local:OpenCloseWindowBehavior WindowType="local:PurpleWindow" Open="{Binding PurpleOpen, Mode=TwoWay}" />
</i:Interaction.Behaviors>
<UserControl.Resources>
<Thickness x:Key="StdMargin">5</Thickness>
<Style TargetType="Button" >
<Setter Property="MinWidth" Value="60" />
<Setter Property="Margin" Value="{StaticResource StdMargin}" />
</Style>
<Style TargetType="Border" >
<Setter Property="Margin" Value="{StaticResource StdMargin}" />
</Style>
</UserControl.Resources>
<Grid>
<StackPanel>
<StackPanel Orientation="Horizontal">
<Border Background="Black" Width="30" />
<Button Content="Open" Command="{Binding OpenBlackCommand}" CommandParameter="True" />
<Button Content="Close" Command="{Binding OpenBlackCommand}" CommandParameter="False" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<Border Background="Yellow" Width="30" />
<Button Content="Open" Command="{Binding OpenYellowCommand}" CommandParameter="True" />
<Button Content="Close" Command="{Binding OpenYellowCommand}" CommandParameter="False" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<Border Background="Purple" Width="30" />
<Button Content="Open" Command="{Binding OpenPurpleCommand}" CommandParameter="True" />
<Button Content="Close" Command="{Binding OpenPurpleCommand}" CommandParameter="False" />
</StackPanel>
</StackPanel>
</Grid>
</UserControl>
YellowWindow (Black/Purple alike):
<Window x:Class="WpfApplication1.YellowWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="YellowWindow" Height="300" Width="300">
<Grid Background="Yellow" />
</Window>
ViewModel, ActionCommand:
using System;
using System.ComponentModel;
using System.Windows.Input;
namespace WpfApplication1
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private bool _blackOpen;
public bool BlackOpen { get { return _blackOpen; } set { _blackOpen = value; OnPropertyChanged("BlackOpen"); } }
private bool _yellowOpen;
public bool YellowOpen { get { return _yellowOpen; } set { _yellowOpen = value; OnPropertyChanged("YellowOpen"); } }
private bool _purpleOpen;
public bool PurpleOpen { get { return _purpleOpen; } set { _purpleOpen = value; OnPropertyChanged("PurpleOpen"); } }
public ICommand OpenBlackCommand { get; private set; }
public ICommand OpenYellowCommand { get; private set; }
public ICommand OpenPurpleCommand { get; private set; }
public ViewModel()
{
this.OpenBlackCommand = new ActionCommand<bool>(OpenBlack);
this.OpenYellowCommand = new ActionCommand<bool>(OpenYellow);
this.OpenPurpleCommand = new ActionCommand<bool>(OpenPurple);
}
private void OpenBlack(bool open) { this.BlackOpen = open; }
private void OpenYellow(bool open) { this.YellowOpen = open; }
private void OpenPurple(bool open) { this.PurpleOpen = open; }
}
public class ActionCommand<T> : ICommand
{
public event EventHandler CanExecuteChanged;
private Action<T> _action;
public ActionCommand(Action<T> action)
{
_action = action;
}
public bool CanExecute(object parameter) { return true; }
public void Execute(object parameter)
{
if (_action != null)
{
var castParameter = (T)Convert.ChangeType(parameter, typeof(T));
_action(castParameter);
}
}
}
}
OpenCloseWindowBehavior:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace WpfApplication1
{
public class OpenCloseWindowBehavior : Behavior<UserControl>
{
private Window _windowInstance;
public Type WindowType { get { return (Type)GetValue(WindowTypeProperty); } set { SetValue(WindowTypeProperty, value); } }
public static readonly DependencyProperty WindowTypeProperty = DependencyProperty.Register("WindowType", typeof(Type), typeof(OpenCloseWindowBehavior), new PropertyMetadata(null));
public bool Open { get { return (bool)GetValue(OpenProperty); } set { SetValue(OpenProperty, value); } }
public static readonly DependencyProperty OpenProperty = DependencyProperty.Register("Open", typeof(bool), typeof(OpenCloseWindowBehavior), new PropertyMetadata(false, OnOpenChanged));
/// <summary>
/// Opens or closes a window of type 'WindowType'.
/// </summary>
private static void OnOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var me = (OpenCloseWindowBehavior)d;
if ((bool)e.NewValue)
{
object instance = Activator.CreateInstance(me.WindowType);
if (instance is Window)
{
Window window = (Window)instance;
window.Closing += (s, ev) =>
{
if (me.Open) // window closed directly by user
{
me._windowInstance = null; // prevents repeated Close call
me.Open = false; // set to false, so next time Open is set to true, OnOpenChanged is triggered again
}
};
window.Show();
me._windowInstance = window;
}
else
{
// could check this already in PropertyChangedCallback of WindowType - but doesn't matter until someone actually tries to open it.
throw new ArgumentException(string.Format("Type '{0}' does not derive from System.Windows.Window.", me.WindowType));
}
}
else
{
if (me._windowInstance != null)
me._windowInstance.Close(); // closed by viewmodel
}
}
}
}
I have handled this sort of situation in the past by bringing in the concept of a WindowManager
, which is a horrible name for it, so let's pair it with a WindowViewModel
, which is only slightly less horrible - but the basic idea is:
public class WindowManager
{
public WindowManager()
{
VisibleWindows = new ObservableCollection<WindowViewModel>();
VisibleWindows.CollectionChanged += OnVisibleWindowsChanged;
}
public ObservableCollection<WindowViewModel> VisibleWindows {get; private set;}
private void OnVisibleWindowsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
// process changes, close any removed windows, open any added windows, etc.
}
}
public class WindowViewModel : INotifyPropertyChanged
{
private bool _isOpen;
private WindowManager _manager;
public WindowViewModel(WindowManager manager)
{
_manager = manager;
}
public bool IsOpen
{
get { return _isOpen; }
set
{
if(_isOpen && !value)
{
_manager.VisibleWindows.Remove(this);
}
if(value && !_isOpen)
{
_manager.VisibleWindows.Add(this);
}
_isOpen = value;
OnPropertyChanged("IsOpen");
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate {};
private void OnPropertyChanged(string name)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
note: I'm just throwing this together very haphazardly; you'd of course want to tune this idea to your specific needs.
But anywho, the basic premise is your commands can work on the WindowViewModel
objects, toggle the IsOpen
flag appropriately, and the manager class handles opening/closing any new windows. There are dozens of possible ways to do this, but it's worked in a pinch for me in the past (when actually implemented and not tossed together on my phone, that is)