0

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");
    }
}
  • not sure this is the best way, you might look at Message passing between classes (viewmodels). Old but probably relevant https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/mvvm-messenger-and-view-services-in-mvvm there are other frameworks that pub/sub style message passing as well like Caliburn.Micro something like this usually employs the use of a base viewmodel – mvermef Aug 03 '23 at 15:53
  • Calling a ViewModel method (e.g. `OnClosing()`) from an MainWindow's `Window_Closing` event handler is perfectly fine. It's not violation of MVVM. There are ways of calling the VM method using XAML, but for start, just call them from codebehind. It's common misconception that with MVVM you should avoid codebehind. Just avoid writing any business logic or complex application logic in code behind, but there's nothing wrong about forwarding events from View to ViewModel. – Liero Aug 04 '23 at 06:41
  • I wouldn't make saving of data just automatic as the user closes down the app. Make it explicit with a save button and then it's just a command like any command. – Andy Aug 05 '23 at 14:13

2 Answers2

0

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>
Sir Rufo
  • 18,395
  • 2
  • 39
  • 73
Jesoo
  • 24
  • 2
0

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);      
Martin
  • 5,165
  • 1
  • 37
  • 50