WPF - How to handle event raised from UserControl in MainWindow and update ViewModel

Viewed 2418

I am trying to raise an event in UserControl1 but I could not figure out how to handle that event to display UserControl2 in MainWindow.

MainWindow.xaml

<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Window.DataContext>
        <local:ViewModel1 />
    </Window.DataContext>

    <Window.Resources>
        <DataTemplate x:Key="Template1" DataType="{x:Type local:ViewModel1}">
            <local:UserControl1 />
        </DataTemplate>
        <DataTemplate x:Key="Template2" DataType="{x:Type local:ViewModel1}">
            <local:UserControl2 />
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ContentControl Content="{Binding }">
            <ContentControl.Style>
                <Style TargetType="{x:Type ContentControl}">
                    <Setter Property="ContentTemplate" Value="{StaticResource Template1}" />
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding UserControlId}" Value="2">
                            <Setter Property="ContentTemplate" Value="{StaticResource Template2}" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    </Grid>
</Window>

MainWindow.cs

using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

UserControl1.xaml

<UserControl x:Class="WpfApp1.UserControl1"
             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:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Label Content="User Control 1" />
        <Button x:Name="btnNext" Content="Next" Click="btnNext_Click"></Button>
    </Grid>
</UserControl>

UserControl1.cs

using System.Windows;
using System.Windows.Controls;

namespace WpfApp1
{
    public partial class UserControl1 : UserControl
    {
        public static readonly RoutedEvent MyEvent = EventManager.RegisterRoutedEvent(
            "MyEventName",
            RoutingStrategy.Bubble,
            typeof(RoutedEventHandler),
            typeof(UserControl1)
        );

        public event RoutedEventHandler LoginEventHandler
        {
            add { AddHandler(MyEvent, value); }
            remove { RemoveHandler(MyEvent, value); }

        }
        public UserControl1()
        {
            InitializeComponent();
        }

        private void btnNext_Click(object sender, RoutedEventArgs e)
        {
            var eventArgs = new RoutedEventArgs(MyEvent);
            RaiseEvent(eventArgs);
        }
    }
}

UserControl2.xaml

<UserControl x:Class="WpfApp1.UserControl2"
             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:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Label Content="User Control 2" />
    </Grid>
</UserControl>

UserControl2.cs

using System.Windows.Controls;

namespace WpfApp1
{
    public partial class UserControl2 : UserControl
    {
        public UserControl2()
        {
            InitializeComponent();
        }
    }
}

ViewModel1.cs

using System.ComponentModel;

namespace WpfApp1
{
    public class ViewModel1 : INotifyPropertyChanged
    {
        public int UserControlId { get; set; }

        public ViewModel1()
        {
            UserControlId = 2;
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

I think i have to update the UserControlId property in ViewModel1 to change the user control but where to handle and how to get the ViewModel1 instance to change the property?

EDIT 1 Sorry for the typo in the ViewModel1 constructor, i meant to initialize the UserControlId property to 1, not 2.

When i run this, i see the window with UserControl1 loaded in the main window. UserControl1 is having a button "Next". On clicking the button in UserControl1, i want to show UserControl2. So I am raising MyEvent routed event inside button click event handler. Now, where to attach and write the event handler for MyEvent and how to get the ViewModel1 instance to change it's UserControlId property?

3 Answers

I finally got this to work. Raised event changed with property name in ViewModel1, added event listener in MainWindow,

Here are the changed files

ViewModel1.cs

using System.ComponentModel;

namespace WpfApp1
{
    public class ViewModel1 : INotifyPropertyChanged
    {
        private int _userControlId;

        public int UserControlId { 
            get { return _userControlId; }

            set 
            {
                _userControlId = value;
                RaisePropertyChanged("UserControlId");
            }
        }

        private void RaisePropertyChanged(string v = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(v));
        }

        public ViewModel1()
        {
            UserControlId = 1;
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

MainWindow.cs

using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            AddHandler(UserControl1.MyEvent, new RoutedEventHandler(OnMyEvent));
        }

        private void OnMyEvent(object sender, RoutedEventArgs e)
        {
            var vm = (ViewModel1)DataContext;
            vm.UserControlId = "2";
        }
    }
}

Don't think the routed event is needed here. Assign a ViewModel to MainWindow and add the Property "UserControlId" in the MainViewModel. Then your XAML code would work properly as you have written a trigger on UserControlId's value at the MainWindow's DataContext level.

In order to do this you need something to mediate between the two view models. One way to mediate is for the main window to be the mediator: it listens to events from the children and then routes them to their required destination. Another way is via an event aggregator. Most MVVM frameworks have one. The best open source one I know of in the WPF world is the one in Caliburn Micro. You subscribe to some kind of message in one view model and then send the event from some other view model and the event aggregator ensures that the event gets routed to its destination. See their documentation for more details.

Related