How to implement registering for a routed event in a ViewModel of a WPF/MVVM application

226 Views Asked by At

Working on a new WPF/MVVM app I "discovered" routed events and thought that might be of good use for communication between different classes. In my sample some custom data is in a ViewModel named MainWindowViewModel, and when closing the application that data should be saved. Managed to define a new custom RoutedEvent in MainWindow.xaml.cs and a way to Raise it when the user closes the application.

Cannot find the correct way to register for this event in MainWindowViewModel, however. EventManager.GetRoutedEvents() shows me (in debug mode) that my custom event is there!

Is there a way to do this in code or am I walking the wrong path here?

<Window x:Class="RoutedEventA.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:vws="clr-namespace:RoutedEventA.Views"
    xmlns:vms="clr-namespace:RoutedEventA.ViewModels"
    mc:Ignorable="d"
    Closing="Window_Closing"
    Title="MainWindow" Height="200" Width="400">

<Window.DataContext>
    <vms:MainWindowViewModel />
</Window.DataContext>

<Grid>
    <vws:MainWindowView/>
</Grid>
namespace RoutedEventA;
public partial class MainWindow : Window
{
    public RoutedEvent CloseTheShop = EventManager.RegisterRoutedEvent(
        "CloseTheShop", RoutingStrategy.Bubble,
        typeof(RoutedEventHandler), typeof(MainWindow));

    private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        Debug.WriteLine("MainWindow Window_Closing");
        RoutedEventArgs close_event = new RoutedEventArgs(CloseTheShop);
        this.RaiseEvent(close_event);
    }
}

<UserControl x:Class="RoutedEventA.Views.MainWindowView"
             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" 
             d:DesignHeight="200" d:DesignWidth="400">
    <Label Content="MainWindowView" />
</UserControl>

namespace RoutedEventA.Views;
public partial class MainWindowView : UserControl
{
    public MainWindowView()
    {
        Debug.WriteLine("MainWindowView ctor");
        InitializeComponent();
    }
}

namespace RoutedEventA.ViewModels;
internal class MainWindowViewModel
{
    public MainWindowViewModel()
    {
        Debug.WriteLine("MainWindowViewModel ctor");
        var gre = EventManager.GetRoutedEvents();
        //
        // My Registered Event is in there!
        // Now to find out how to regsiter for my custom event and call the Save method.
        //

    }
    //
    // The Save method is for all kinds of data of this ViewModel (to be desigend).
    //
    public void Save()
    {
        Debug.WriteLine("MainWindow Save");
    }
}
2

There are 2 best solutions below

0
Jesoo On

The short answer is simple: you can't do that.

The long answer is quite articulated: as I said before, you can't register events directly from view to viewmodel, but you could use a workaround with a nuget package. First of all, reference Microsoft.Xaml.Behaviors.Wpf package in your project: https://www.nuget.org/packages/Microsoft.Xaml.Behaviors.Wpf

Then, reference it in your window

<Window x:Class="RoutedEventA.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:vws="clr-namespace:RoutedEventA.Views"
    xmlns:vms="clr-namespace:RoutedEventA.ViewModels"
    xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"
    mc:Ignorable="d"
    Title="MainWindow" Height="200" Width="400">

    <Window.DataContext>
        <vms:MainWindowViewModel />
    </Window.DataContext>

    <Grid>
        <vws:MainWindowView/>
    </Grid>

So, create an appropriate ICommand in your viewmodel to handle the event raising and bind your event to it (for reasons of length I will not explain how to do it, but you will find all the information you need in this link: ICommand MVVM implementation)

<Window x:Class="RoutedEventA.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:vws="clr-namespace:RoutedEventA.Views"
    xmlns:vms="clr-namespace:RoutedEventA.ViewModels"
    xmlns:behavior="http://schemas.microsoft.com/xaml/behaviors"
    mc:Ignorable="d"
    Title="MainWindow" Height="200" Width="400">

<Window.DataContext>
    <vms:MainWindowViewModel />
</Window.DataContext>

<behavior:Interaction.Triggers>
    <behavior:EventTrigger EventName="Closing">
        <behavior:InvokeCommandAction Command="{Binding WindowClosingCommand}" />
    </behavior:EventTrigger>
</behavior:Interaction.Triggers>
<Grid>
    <vws:MainWindowView/>
</Grid>
2
Martin On

WPF events are usually used for connecting events that are raised by UI controls to event handlers in the code behind file.

I think what you have in your case is more like an application event, that might be consumed anywhere, not just in code behind files.


PRISM FOR WPF - EVENT AGGREGATOR

Maybe you should take a look at the "Prism for WPF" library, see Prism for WPF.

You can use a feature called "event aggregator". This enables you to publish events and subscribe to events using a IEventAggregator interface. You can do this anywhere in your code. The class that publishes the event and the class(es) that handle the event do not need to know each other.

The following example code is taken from here: https://prismlibrary.com/docs/event-aggregator.html

Define a data class that holds the events data. This class derives from PRISMS PubSubEvent<T> class and could define additional properties:

public class TickerSymbolSelectedEvent : PubSubEvent<string>{}

Provide an instance of type IEventAggregator to the class, that wants to subscribe to an event. Use the IEventAggregators GetEvent method to get a reference to the event. Then attach an event handling method (ShowNews in this case):

    public class MainPageViewModel
    {
        public MainPageViewModel(IEventAggregator ea)
        {
            ea.GetEvent<TickerSymbolSelectedEvent>().Subscribe(ShowNews);
        }
    
        void ShowNews(string companySymbol)
        {
            //implement logic
        }
    }
    
    

Now on how to publish an event (in another class). This is almost identical like subscribing to an event. But instead of Subscribe, you use the Publish method in the IEventAggregator:

public class SomeOtherClass
{
    IEventAggregator _eventAggregator;
    public MainPageViewModel(IEventAggregator ea)
    {
        _eventAggregator = ea;
    }

    public void CreateAndPublishEvent()
    {
        _eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish("STOCK0");
    }
}

Finally, create an EventAggregator instance somewhere in the startup logic of you application, for example in the App.xaml.cs file. From there, pass it somehow to the viewmodels and other classes that want to use it. Always pass the same instance!

    EventAggregator ea = new EventAggregator(); // implements IEventAggregator
    MainPageViewModel viewModel = new MainPageViewModel(ea);