19

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?

SiberianGuy
  • 24,674
  • 56
  • 152
  • 266

5 Answers5

48

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... :)

enter image description here

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
            }
        }
    }
}
Mike Fuchs
  • 12,081
  • 6
  • 58
  • 71
  • 1
    @adabyron, Why don't you give your answer as a downloadable source code? – RobinAtTech Nov 03 '14 at 05:31
  • 2
    Love this solution! I changed it to Behavior in order to be able to use it for both Windows and UserControls – Petter Jan 18 '19 at 09:53
  • 2
    I've come across this answer linked from others a few times. So since I keep coming back here, I'd point out that the only thing I keep missing is the why of it...as in "why would I use this over a service/messenger approach?" – DonBoitnott Feb 04 '19 at 13:27
  • While the answer is very good, this is **not** a clean use case of `Behavior`s. In fact, this answer "mixes" different intended `Behavior` contexts, as clarified in [this answer](https://stackoverflow.com/a/30505091/10102452). – Vector Sigma Dec 16 '19 at 23:01
  • @VectorSigma You missed the **attached** (property) part. This behaviour is registering regular dependency properties. – Mike Fuchs Dec 18 '19 at 20:33
  • @MikeFuchs Indeed, you are **perfectly correct**, I did miss the attached part! In any case, what I wanted to point out is that the main point of `Behavior`s is so that one can use the `OnAttached` and `OnDetached` methods, to enhance the _behavior_ of attached objects. This use case here is only based on the registered dependency properties. The class is a `Behavior` just for the convenience of being able to add it easily to a `BehaviorCollection` in the XAML code of a control. – Vector Sigma Dec 18 '19 at 22:24
  • 2
    @DonBoitnott There are some **significant benefits** that this method offers. Consider using a service/messenger approach. Suddenly, you wish to define a `PlacementTarget` for the window (e.g. mouse position, or Left/Right/Top/Bottom etc. with respect to a UIControl). Or you wish to define offsets, or other tricks up the sleeve of a `Popup`, for example. In a service/messenger approach, these have to be **exposed** as parameters in the public API of the service, let alone the service may be called, e.g., from within a view model, which has no idea about the UI. (**continued**) – Vector Sigma Dec 18 '19 at 22:31
  • 2
    @DonBoitnott (**continued**) Service approaches (the [ones](https://stackoverflow.com/a/16653029/10102452) around [here](https://stackoverflow.com/a/25846192/10102452) anyway) typically stop at passing an `object`, which is supposed to be the `Window.Content` to be assigned. Using the approach of **this** answer additionally gives you the capability to simply register a dependency property such as `PlacementTarget` (a e.g. `UIElement`) and, suddenly, **you can set it in XAML** (where all UI information is available). So you can set stuff from a `ViewModel` and a `UserControl` at once! – Vector Sigma Dec 18 '19 at 22:39
6

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)

JerKimball
  • 16,584
  • 3
  • 43
  • 55
5

A reasonable way for purists is creating a service that handles your navigation. Short summary: create a NavigationService, register your view at the NavigationService and use the NavigationService from within the view model to navigate.

Example:

class NavigationService
{
    private Window _a;

    public void RegisterViewA(Window a) { _a = a; }

    public void CloseWindowA() { a.Close(); }
}

To get a reference to NavigationService you could make an abstraction on top of it (i.e. INavigationService) and register/get it via a IoC. More properly you could even make two abstractions, one that contains the methods for registration (used by the view) and one that contains the actuators (used by the view model).

For a more detailed example you could check out the implementation of Gill Cleeren which heavily depends on IoC:

http://www.silverlightshow.net/video/Applied-MVVM-in-Win8-Webinar.aspx starting at 00:36:30

Sander
  • 616
  • 1
  • 6
  • 17
4

One way to achieve this would be for the view-model to request that the child windows should be closed:

public class ExampleUserControl_ViewModel
{
    public Action ChildWindowsCloseRequested;

    ...
}

The view would then subscribe to its view-model's event, and take care of closing the windows when it's fired.

public class ExampleUserControl : UserControl
{
    public ExampleUserControl()
    {
        var viewModel = new ExampleUserControl_ViewModel();
        viewModel.ChildWindowsCloseRequested += OnChildWindowsCloseRequested;

        DataContext = viewModel;
    }

    private void OnChildWindowsCloseRequested()
    {
        // ... close child windows
    }

    ...
}

So here the view-model can ensure the child windows are closed without having any knowledge of the view.

Oli Wennell
  • 846
  • 1
  • 7
  • 10
  • 4
    You can also set the DataContext of the UserControl to your ViewModel, getting rid of the ViewModel public property. This will require some casting on the event registration but is a good practice since in MVVM you need to be setting the UserControl.DataContext to ViewModel anyways. Also, be sure to perform some validation that your ChildWindowsCloseRequested is not null before calling it, or you will get an exception. – Michael Sanderson Mar 19 '13 at 19:55
2

Most answers to this question involve a state variable that is controlled by the ViewModel and the View acts on changes to this variable. This is good for stateful commands like opening or closing a window, or simply showing or hiding some controls. It doesn't work well for stateless event commands though. You could trigger some action on the rising edge of the signal but need to set the signal to low (false) again or it won't ever trigger again.

I have written an article about the ViewCommand pattern which solves this problem. It is basically the reverse direction of regular Commands that go from the View to the current ViewModel. It involves an interface that each ViewModel can implement to send commands to all currently connected Views. A View can be extended to register with each assigned ViewModel when its DataContext property changes. This registration adds the View to the list of Views in the ViewModel. Whenever the ViewModel needs to run a command in a View, it goes through all registered Views and runs the command on them if it exists. This makes use of reflection to find the ViewCommand methods in the View class, but so does Binding in the opposite direction.

The ViewCommand method in the View class:

public partial class TextItemView : UserControl
{
    [ViewCommand]
    public void FocusText()
    {
        MyTextBox.Focus();
    }
}

This is called from a ViewModel:

private void OnAddText()
{
    ViewCommandManager.Invoke("FocusText");
}

The article is available on my website and in an older version on CodeProject.

The included code (BSD licence) provides measures to allow renaming methods during code obfuscation.

ygoe
  • 18,655
  • 23
  • 113
  • 210