1

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.

Sharada Gururaj
  • 13,471
  • 1
  • 22
  • 50
Graviton
  • 81,782
  • 146
  • 424
  • 602
  • Do you really *need* to simulate the button click? Why not just directly invoke the `ICommand` the button is bound to? – Bradley Uffner Jul 11 '17 at 02:10
  • I don't _actually_ want to simulate button click-- just want to do an event, where the VM is the publisher and the view is the subscriber. – Graviton Jul 11 '17 at 02:30
  • Ahh, that makes much more sense. Typically in MVVM you would use some kind of messaging system to handle things like this. Most MVVM libraries include one. MVVMLight uses a class called `Messenger`, there is a good tutorial on it [here](http://jesseliberty.com/2011/01/06/windows-phone-from-scratch%E2%80%93mvvm-light-toolkit-soup-to-nuts-3/). If you aren't using a framework, or the one you are using doesn't have one, you would have to build something like it yourself. – Bradley Uffner Jul 11 '17 at 02:33
  • As a note, in case DataContext does not hold a ViewModelBase, `(DataContext as ViewModelBase).MyEvent` will wrongly result in a NullReferenceException, where the explicit cast `((ViewModelBase)DataContext).MyEvent` will correctly give you an InvalidCastException. – Clemens Jul 11 '17 at 05:34
  • @Graviton do you really need event ? You can bind events to ICommand with argument – Eldho Jul 11 '17 at 07:08
  • @Eldho, maybe you can show me how you do it? – Graviton Jul 11 '17 at 07:09
  • Explain exactly what do you want. What is the event in VM and what should happen in View? – Liero Jul 11 '17 at 07:28
  • @Liero, The VM will receive a command from other VMs ( think of `FileVM` notifies `tabVM`), and then the VM (`tabVM`) will load the data at the view via the `SimulateClick` method at View (`tabView`). – Graviton Jul 11 '17 at 07:35
  • @Graviton: Have updated my answer to make it more generic - you can now specify the event you want to track on view-model – Sharada Gururaj Jul 20 '17 at 15:42

5 Answers5

2

If you view model needs to tell the view to do something you could use an event aggregator or a messenger to send a message from the view model to the view in a loosely coupled way:

https://blog.magnusmontin.net/2014/02/28/using-the-event-aggregator-pattern-to-communicate-between-view-models/

https://msdn.microsoft.com/en-us/magazine/jj694937.aspx

The benefit of using this pattern is that the view model and the view don't need to know anything about each other.

The other option would be to inject the view model with an interface type that the view implements:

public interface IView
{
    bool SimulateClick(object param);
}

public partial class View : UserControl, IView
{
    public View()
    {
        InitializeComponent();
        DataContext = new ViewModel(this);
    }

    public bool SimulateClick(object param)
    {
        //...
    }

}

This doesn't really break the MVVM pattern as the view model only has a dependency upon an interface that the view happens to implement.

mm8
  • 163,881
  • 10
  • 57
  • 88
1

The VM will receive a command from other VMs ( think of FileVM notifies tabVM), and then the VM (tabVM) will load the data at the view via the SimulateClick method at View (tabView).

Why the hell would you name method SimulateClick if the method loads data based on some command? I would refactor your code like this.

public delegate bool MyDel(object data);

public class ViewModelBase
{
   public MyDel SomeCommandExecuted;

   void SomeCommand_Execute()
   {
      string[] sampleData = new [] //this may come from the other viewmodel for example
      {
         "Item1", "Item2", "Items3";
      }

      MyDel handler = SomeCommandExecuted;
      if (handler != null)
      {
          handler(sampleData);
      }
   }
}

if you exposed the functionality like this, it is ok to attach to the event in codebehind. Why would you pollute XAML with attaching to the VM's event and calling codebehind method? It's better to attach to the event in codebehind, because at least your code remains type safe and refactorable.

public class View: UserControl
{
   public View()
   {
       this.InitializeComponent();
       Loaded += (o, e) => ViewModel.SomeCommandExecuted += ViewModel_SomeCommandExecuted;
   }

   ViewModelBase ViewModel => (ViewModelBase)DataContext;


  private bool ViewModel_SomeCommandExecuted(object data)  
  { 
     //load the data into view
  }
}

Attaching to the VM's event in codebehind is not violation of MVVM. However, the resposibility of ViewModel is to expose data in such form that is easily consumable from View (usually via databinding).

here is my suggestion:

public class ViewModelBase : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;

   protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
   {
       PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
   }

   protected bool InDesignMode => DesignerProperties.GetIsInDesignMode(new DependecyObject());
}

public class ViewModel : ViewModelBase
{
   private string[] _data;

   //ctor
   public ViewModel()
   {
       if (IsInDesignMode)
       {
           Data = new [] { "Visible", "In", "XAML", "Designer" }
       }
   }

   public string[] Data
   {
      get { return _data; }
      set { _data = value; OnPropertyChanged(); }
   }

   void SomeCommand_Execute()
   {
      string[] sampleData = new [] //this may come from the other viewmodel for example
      {
         "Item1", "Item2", "Items3";
      }

      Data = sampleData;
   }
}

I have prepared the data in ViewModel so that they are easily consumable in view. Once they are ready, I notify view using PropertyChanged event. Now I can easily bind ItemsControl, ListView, etc in View. No codebehind needed what's so ever. This is the purpose of ViewModel

Liero
  • 25,216
  • 29
  • 151
  • 297
1

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>
Sharada Gururaj
  • 13,471
  • 1
  • 22
  • 50
-1

You can use x:bind for function binding. That way your xaml can bind and directly invoke the view models event handler without needing a "pass through" invoke method in the view.

Click="{x:Bind viewModel.Foo}"

More docs

Here is an example of event binding in wpf

WPF event binding from View to ViewModel?

Frank Sposaro
  • 8,511
  • 4
  • 43
  • 64
-1

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"

 <StackPanel Background="Transparent">
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="Tap">
      <command:EventToCommand
        Command="{Binding Main.NavigateToArticleCommand,
          Mode=OneWay,
          Source={StaticResource Locator}}"
        CommandParameter="{Binding Mode=OneWay}" />
    </i:EventTrigger>
  </i:Interaction.Triggers>
</StackPanel>

Viewmodel

public RelayCommand NavigateToArticleCommand 
{ 
 get 
{ 

  return _navigateToArticleCommand
      ?? (_navigateToArticleCommand= new RelayCommand( 
        async () => 
        { 
          await SomeCommand(); 
        })); 
  } 
}
Eldho
  • 7,795
  • 5
  • 40
  • 77
  • 1
    If I am not mistaken, what you are doing is the View will first get triggered via `Tap` event, and then it notifies the ViewModel, am I right? But what I want is exactly the reverse. – Graviton Jul 11 '17 at 07:36
  • Theoretically, you could do it also the opposite way by setting EventTrigger.SourceObject to ViewModel, but that's weird and you should consider refactoring your code. – Liero Jul 11 '17 at 08:25
  • @Liero , why do you say it's weird ? I – Graviton Jul 11 '17 at 09:51
  • Why would you want to use 9 lines of dirty xaml code instead of one or two lines of clean C# code, when the logic is in codebehind anyway? – Liero Jul 11 '17 at 10:01
  • In my cases i would use the command in the viewmodel instead of view if its not interacting with UI elements – Eldho Jul 11 '17 at 10:24
  • This seems to be the opposite of what was asked – StayOnTarget Feb 01 '21 at 17:16