Graviton Graviton - 1 year ago 54
C# Question

Bind ViewModel's event to XAML

In my VM, I have an event

public class ViewModelBase
{
public delegate bool MyDel(object param);
public MyDel MyEvent;

public void TransferClick()
{
MyEvent(null); // to simulate the click at View
}
}


And in the View, currently I have the following code ( behind):

public class View: UserControl
{
private void UserControl1_Load(Object sender, EventArgs e)
{

(DataContext as ViewModelBase).MyEvent+=SimulateClick;

}
private bool SimulateClick(object param)
{
//some logic to simulate clicks on the View, on the user control
}
}


So that the VM can invoke the
SimulateClick
logic in View whenever it has to.

I don't like this approach because it pollutes my view's code behind. Any way to make the
MyEvent
bind to XAML instead, much like how I bind VM
ICommand
to existing button clicks and stuff like that?

Note: I don't actually want to simulate mouse clicks ( I know I can use
ICommand
to do just that), just want to do some events like mouse clicks event on my MVVM model.

Answer Source

Updated answer

First of all - I would highly recommend the approach @mm8 has suggested, or exposing Command(s) (such as RefreshCommand) on your views to achieve the same.

But if that is not an option; then I believe you can create a custom attached event that can technically bind the view-model's event to the control's eventhandler; while maintaining the MVVM level of separation.

For example, you can define an attached event in following manner:

// ViewModel event args 
public class MyEventArgs : EventArgs
{
    public object Param { get; set; }
}

// Interim args to hold params during event transfer    
public class InvokeEventArgs : RoutedEventArgs
{
    public InvokeEventArgs(RoutedEvent e) : base(e) { }

    public object Param { get; set; }
}    

// Base view model
public class ViewModelBase
{
    public event EventHandler<MyEventArgs> MyEvent1;
    public event EventHandler<MyEventArgs> MyEvent2;

    public void TransferClick1()
    {
        MyEvent1?.Invoke(this, new MyEventArgs { Param = DateTime.Now }); // to simulate the click at View
    }

    public void TransferClick2()
    {
        MyEvent2?.Invoke(this, new MyEventArgs { Param = DateTime.Today.DayOfWeek }); // to simulate the click at View
    }
}

// the attached behavior that does the magic binding
public class EventMapper : DependencyObject
{
    public static string GetTrackEventName(DependencyObject obj)
    {
        return (string)obj.GetValue(TrackEventNameProperty);
    }

    public static void SetTrackEventName(DependencyObject obj, string value)
    {
        obj.SetValue(TrackEventNameProperty, value);
    }

    public static readonly DependencyProperty TrackEventNameProperty =
        DependencyProperty.RegisterAttached("TrackEventName",
            typeof(string), typeof(EventMapper), new PropertyMetadata
            (null, new PropertyChangedCallback(OnTrackEventNameChanged)));

    private static void OnTrackEventNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs args)
    {
        FrameworkElement uie = d as FrameworkElement;
        if (uie == null)
            return;

        var eventName = GetTrackEventName(uie);
        if (string.IsNullOrWhiteSpace(eventName))
            return;

        EventHandler<MyEventArgs> vmEventTracker = delegate (object sender, MyEventArgs e) {
            Application.Current.Dispatcher.Invoke(() =>
                uie.RaiseEvent(new InvokeEventArgs(EventMapper.OnInvokeEvent)
                {
                    Source = sender,
                    Param = e?.Param
                }));
        };

        uie.DataContextChanged += (object sender, DependencyPropertyChangedEventArgs e) =>
        {
            var oldVM = e.OldValue;
            var newVM = e.NewValue;

            if (oldVM != null)
            {
                var eventInfo = oldVM.GetType().GetEvent(eventName);
                eventInfo?.RemoveEventHandler(oldVM, vmEventTracker);
            }

            if (newVM != null)
            {
                var eventInfo = newVM.GetType().GetEvent(eventName);
                eventInfo?.AddEventHandler(newVM, vmEventTracker);
            }
        };

        var viewModel = uie.DataContext;
        if (viewModel != null)
        {
            var eventInfo = viewModel.GetType().GetEvent(eventName);
            eventInfo?.AddEventHandler(viewModel, vmEventTracker);
        }
    }

    public static readonly RoutedEvent OnInvokeEvent =
        EventManager.RegisterRoutedEvent("OnInvoke",
            RoutingStrategy.Direct, typeof(RoutedEventHandler), typeof(EventMapper));
    public static void AddOnInvokeHandler(DependencyObject d, RoutedEventHandler handler)
    {
        FrameworkElement uie = d as FrameworkElement;
        if (uie != null)
        {
            uie.AddHandler(OnInvokeEvent, handler);
        }
    }

    public static void RemoveOnInvokeHandler(DependencyObject d, RoutedEventHandler handler)
    {
        FrameworkElement uie = d as FrameworkElement;
        if (uie != null)
        {
            uie.RemoveHandler(OnInvokeEvent, handler);
        }
    }
}

Sample 1 - Event handler

XAML Usage

<StackPanel Margin="20">
    <Button Margin="10" Content="Invoke VM event" Click="InvokeEventOnVM" />        
    <Button Content="View Listener1" 
            local:EventMapper.TrackEventName="MyEvent1"
            local:EventMapper.OnInvoke="SimulateClick1" />

    <Button Content="View Listener2" 
            local:EventMapper.TrackEventName="MyEvent1"
            local:EventMapper.OnInvoke="SimulateClick1" />

    <Button Content="View Listener3" 
            local:EventMapper.TrackEventName="MyEvent2"
            local:EventMapper.OnInvoke="SimulateClick2" />

</StackPanel>

Sample code-Behind for above XAML:

private void SimulateClick1(object sender, RoutedEventArgs e)
{
    (sender as Button).Content = new TextBlock { Text = (e as InvokeEventArgs)?.Param?.ToString() };
}

private void SimulateClick2(object sender, RoutedEventArgs e)
{
    SimulateClick1(sender, e);
    (sender as Button).IsEnabled = !(sender as Button).IsEnabled; //toggle button
}

private void InvokeEventOnVM(object sender, RoutedEventArgs e)
{
    var vm = new ViewModelBase();
    this.DataContext = vm;

    vm.TransferClick1();
    vm.TransferClick2();
}

enter image description here

Sample 2 - Event Trigger (updated 07/26)

XAML Usage

<Button Content="View Listener" 
    local:EventMapper.TrackEventName="MyEvent2">
    <Button.Triggers>
        <EventTrigger RoutedEvent="local:EventMapper.OnInvoke">
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation AutoReverse="True" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:1" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Button.Triggers>
</Button>
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download