'How do I close my MVVM popup using the MVVM format without a framework

First let me start by stating I did go through about 40 similar questions here on stack overflow (and lots of google results), a majority of answers use some sort of framework. The few answers that don't use a framework link to their blog post where they wrote several hundreds lines creating a generic method, or some overly complicated solution that doesn't really apply to my scenario. Some of the answers link to an already answered post that is 10 or more years old, and the answer is using a method that is outdated by newer releases, and doesn't work.

I am trying to create a simple error popup. The popup has a label and an ok button.

Popup

Pressing the OK button on the popup should close the popup.

I have everything working but the close. I just need a simple solution that follows MVVM so that I can unit test the code. I don't need a framework that will fit hundreds of different scenarios, I just need the popup to close.

I have tried almost every suggested answer here on stack overflow, and ones I found googling. But I was unable to get any working. After 2 and a half days I've given up and am looking for an answer. I'm sure I was close at some point but...

I created a stripped down version of my project that is below. Can anyone show me what I need to do in order to close the popup?

MainWindow.xaml

<Window.DataContext>
    <local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
    <Button HorizontalAlignment="Center" VerticalAlignment="Center" Content="Open Dialog" Command="{Binding ShowErrorPopup}"/>
</Grid>

MainWindowViewModel.cs

class MainWindowViewModel
{
    public ICommand ShowErrorPopup
    {
        get;
        set;
    }

    public MainWindowViewModel()
    {
        ShowErrorPopup = new RelayCommand(new Action<object>(ExecuteShowErrorPopup));
    }

    public void ExecuteShowErrorPopup(object obj)
    {
        ErrorPopupService errorPopupNav = new ErrorPopupService();
        errorPopupNav.CreateErrorPopup("Test Message");
    }
}

ErrorPopup.xaml

 <Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Label Grid.Row="0" Content="{Binding ErrorMessage}" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="Red" FontSize="14" FontWeight="Bold"/>
    <Button Grid.Row="1" Content="OK" Width="100" Margin="4" HorizontalAlignment="Right" Command="{Binding CloseWindowCommand, Mode=OneWay}" CommandParameter="{Binding ElementName=ErrorPopupWindow}"/>
</Grid>

ErrorPopupViewModel.cs

class ErrorPopupViewModel
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private String m_errorMessage;

    public String ErrorMessage
    {
        get
        {
            return m_errorMessage;
        }
        private set
        {
            m_errorMessage = value;
            OnPropertyChanged(ErrorMessage);
        }
    }

    public ErrorPopupViewModel(String ErrorMessageToDisplay)
    {
        ErrorMessage = ErrorMessageToDisplay;
    }
}

ErrorPopupService.cs

interface ErrorPopupServiceInterface
{
    void CreateErrorPopup(String ErrorMessage);
}

class ErrorPopupService : ErrorPopupServiceInterface
{
    private ErrorPopup m_errorPopup;

    public void CreateErrorPopup(String ErrorMessage)
    {
        m_errorPopup = new ErrorPopup
        {
            DataContext = new ErrorPopupViewModel(ErrorMessage)
        };
        m_errorPopup.Show();
    }
}

RelayCommand.cs

class RelayCommand : ICommand
{
    private Action<object> _action;

    public RelayCommand(Action<object> action)
    {
        _action = action;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        _action(parameter);
    }
}


Solution 1:[1]

If you want to be able to close the popup from the view model, your ErrorPopupService should return some kind of reference to the popup, e.g.:

class ErrorPopupService : ErrorPopupServiceInterface
{
    public IPopup CreateErrorPopup(String ErrorMessage)
    {
        ErrorPopup popup = new ErrorPopup
        {
           DataContext = new ErrorPopupViewModel(ErrorMessage)
        };
        popup.Show();
        return popup;
    }
}

IPopup is an interface:

public interface IPopup
{
    void Close();
}

...that ErrorPopup implements:

public partial ErrorPopup : Window, IPopup { ... }

The view model knows only about this interface which means that you can mock the popup in your unit tests:

ErrorPopupService errorPopupNav = new ErrorPopupService();
IPopup popup;

public void ExecuteShowErrorPopup(object obj)
{
    popup = errorPopupNav.CreateErrorPopup("Test Message");
}

public void ExecuteCloseErrorPopup(object obj)
{
    popup?.Close();
}

If you don't want to expose the popup, the other options is to add a void Close() method to your ErrorPopupService that simply closes m_errorPopup:

public void CloseErrorPopup()
{
    m_errorPopup?.Close();
}

Solution 2:[2]

After working on this more I was able to find a solution. I am passing in the Dialogs close command as an action to the dialogs view model. I have posted the bare minimum code that I used below.

If there is a problem with this solution, breaking the MVVM standards, or there is a better, simpler solution please post it.

ErrorPopup.xaml, and ErrorPopupViewModel.cs were in a folder named Dialogs. ObservableObjects.cs, and RelayCommand.cs were in a folder named Helpers

MainWindow.xaml

<Window x:Class="Dialog.MainWindow"
    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:Dialog"
    mc:Ignorable="d"
    Title="MainWindow" Height="150" Width="400">
<Window.DataContext>
    <local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
    <Button  Command="{Binding NewModelessDialogCommand}" HorizontalAlignment="Center" VerticalAlignment="Center" Content="Open Dialog" />
</Grid>
</Window>

MainWindowViewModel.cs

using System;
using System.Windows.Input;
using Dialog.Helpers;
using Dialog.Dialogs;

namespace Dialog
{
    class MainWindowViewModel
    {
        private ErrorPopup m_errorPopup;        
        private ICommand m_newModelessDialogCommand;

        public ICommand NewModelessDialogCommand
        {
            get
            {
                return m_newModelessDialogCommand;
            }
            private set
            {
                m_newModelessDialogCommand = value;
            }
        }

        public MainWindowViewModel()
        {
            NewModelessDialogCommand = new RelayCommand(new Action<object>(OpenDialog));
        }

        public void OpenDialog(object obj)
        {
            m_errorPopup = new ErrorPopup
            {
                DataContext = new ErrorPopupViewModel(() => CloseDialog())
                {
                    Message = "Error Message!"
                }
            };
            m_errorPopup.Show();
        }

        public void CloseDialog()
        {
            if (m_errorPopup != null)
            {
                m_errorPopup.Close();
            }
        }
    }
}

ErrorPopup.xaml

<Window x:Class="Dialog.Dialogs.ErrorPopup"
    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:Dialog.Dialogs"
    mc:Ignorable="d"
    WindowStartupLocation="CenterOwner" WindowStyle="None"
    Title="{Binding Caption}" Height="170" Width="300" ResizeMode="NoResize" >
<Grid>
    <TextBlock Text="{Binding Message}" HorizontalAlignment="Center" Margin="0,50,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
    <Button Content="Ok" Command="{Binding OkCommand}" HorizontalAlignment="Center" Margin="0,100,0,0" VerticalAlignment="Top" Width="75" IsDefault="True" />
</Grid>

ErrorPopup.cs

using System;
using System.Windows.Input;
using Dialog.Helpers;

namespace Dialog.Dialogs
{   
    public class ErrorPopupViewModel : ObservableObject
    {
        private string m_message;
        private ICommand m_OkCommand;
        private Action m_closeCommand;

        public string Message
        {
            get 
            { 
                return m_message; 
            }
            set 
            { 
                m_message= value; 
                RaisePropertyChanged("Message"); 
            }
        }    

        public ICommand OkCommand
        {
            get 
            {
                return m_OkCommand;
            }
            private set 
            {
                m_OkCommand = value;
            }
        }

        public ErrorPopupViewModel(Action closeCommand)
        {
            OkCommand = new RelayCommand(new Action<object>(Ok));

            m_closeCommand = closeCommand;
        }

        public void Ok(object obj)
        {
            m_closeCommand();
        }
    }
}

ObservableObject.cs

using System;
using System.ComponentModel;
using System.Linq.Expressions;

namespace Dialog.Helpers
{
    public class ObservableObject : INotifyPropertyChanged
    {
        protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)

        {
            var memberExpr = propertyExpression.Body as MemberExpression;

            if (memberExpr == null)
            {
                throw new ArgumentException("propertyExpression should represent access to a member");
            }
            string memberName = memberExpr.Member.Name;
            RaisePropertyChanged(memberName);
        }
    }
}

RelayCommand.cs

using System;
using System.Windows.Input;

namespace Dialog.Helpers
{
    public class RelayCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;
        private Action<object> m_action;

        public RelayCommand(Action<object> action)
        {
           m_action= action;
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            m_action(parameter);
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 mm8
Solution 2