72

I’m doing some refactoring of a simple application to follow MVVM and my question is how do I move a SelectionChanged event out of my code behind to the viewModel? I’ve looked at some examples of binding elements to commands but didn’t quite grasp it. Can anyone assist with this. Thanks!

Can anyone provide a solution using the code below? Many thanks!

public partial class MyAppView : Window 
{
    public MyAppView()
    {
        InitializeComponent();

        this.DataContext = new MyAppViewModel ();

        // Insert code required on object creation below this point.
    }

    private void contactsList_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
    {
        //TODO: Add event handler implementation here.           
        //for each selected contact get the labels and put in collection 

        ObservableCollection<AggregatedLabelModel> contactListLabels = new ObservableCollection<AggregatedLabelModel>();

        foreach (ContactListModel contactList in contactsList.SelectedItems)
        {
            foreach (AggregatedLabelModel aggLabel in contactList.AggLabels)
            {
                contactListLabels.Add(aggLabel);
            }
        }
        //aggregate the contactListLabels by name
        ListCollectionView selectedLabelsView = new ListCollectionView(contactListLabels);

        selectedLabelsView.GroupDescriptions.Add(new PropertyGroupDescription("Name"));
        tagsList.ItemsSource = selectedLabelsView.Groups;
    }
}
Ben
  • 1,113
  • 5
  • 15
  • 25
  • Code-behind is not relevant in terms of MVVM. MVVM is a design pattern and code-behind is a compiler feature. By definition, a design pattern must be compiler and language agnostic. MVVM is an architectural design pattern that doesn't care about class level design problems. Therefore it's absolutely fine to have code-behind event handlers. Avoiding them only adds unnecessary complexity to your code. There are scenarios where you can't avoid code-behind. In MVVM all that matters is the relationship between classes and their responsibilities (on application level). – BionicCode Apr 04 '23 at 19:53
  • In other words, the only way to violate MVVM is to violate the mandatory dependency graph (*View ---> View Model ---> Model*) or class responsibilities in regards to the MVVM application component model (e.g. wrong responsibility is part of the wrong component, for example handling dialogs is part of the View Model or Model). – BionicCode Apr 04 '23 at 19:53
  • An alternative is to bind the `SelectedItem` property to the view model class and handle changes in the property setter. – BionicCode Apr 04 '23 at 20:00

11 Answers11

134

You should use an EventTrigger in combination with InvokeCommandAction from the Windows.Interactivity namespace. Here is an example:

<ListBox ...>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectionChanged">
            <i:InvokeCommandAction Command="{Binding SelectedItemChangedCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</ListBox>

You can reference System.Windows.Interactivity by going Add reference > Assemblies > Extensions.

And the full i namespace is: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity".

BitSchupser
  • 432
  • 3
  • 13
Pavlo Glazkov
  • 20,498
  • 3
  • 58
  • 71
  • Thanks. I'm a newbie to programming so forgive me - are you able to give an example using the code I provided? – Ben Feb 06 '11 at 21:49
  • 1
    You basically need to create a command property in your ViewModel called "SelectedItemChangedCommand". Commanding is similar to events, but a command can only have one callback function, unlike events. Check the docs: http://msdn.microsoft.com/en-us/library/ms752308.aspx – Brian Feb 13 '11 at 01:55
  • 1
    If you do not have Expression Blend, you will need the SDK: http://www.microsoft.com/downloads/en/details.aspx?FamilyID=D197F51A-DE07-4EDF-9CBA-1F1B4A22110D&displaylang=en – Murven Feb 14 '11 at 04:06
  • 4
    You also need to be targeting .net framework 4.0. This behavior action is not in the 3.5 framework, even if you're using Blend4 – Bill Tarbell Oct 20 '12 at 20:57
  • Pavlo, thank you for that Day-Making-hint! I'm trying to call a command to handle delete event in DataGridView. How would I get the same object parameter (dataset object in my case) event handler gets? – Roaring Stones Feb 26 '13 at 08:38
  • @RoaringStones You are welcome! Regarding your question, please see this: http://stackoverflow.com/questions/6205472/mvvm-passing-eventargs-as-command-parameter. – Pavlo Glazkov Feb 27 '13 at 17:46
  • Top answer. I've been looking for DAYS for this particular solution... – AndyUK Jan 06 '17 at 10:18
  • If you also need the `eventargs` parameter to be passed to your command, then use `prism:InvokeCommandAction`. See this answer for more details https://stackoverflow.com/a/55593640/4856020 – datchung Apr 09 '19 at 13:09
  • 1
    @datchung prism is not longer need if one uses the nuget package Microsoft.Xaml.Behaviors.Wpf because it gives access to an updated version of the InvokeCommandAction class which has now a property called "PassEventArgsToCommand". Here is the [source](https://github.com/microsoft/XamlBehaviorsWpf/blob/master/src/Microsoft.Xaml.Behaviors/InvokeCommandAction.cs) – Xam Oct 29 '19 at 04:10
11

To refactor this you need to shift your thinking. You will no longer be handling a "selection changed" event, but rather storing the selected item in your viewmodel. You would then use two-way data binding so that when the user selects an item, your viewmodel is updated, and when you change the selected item, your view it updated.

sourcenouveau
  • 29,356
  • 35
  • 146
  • 243
  • I think the shifing thinking part is where i'm struggling being a fairly novice programmer! – Ben Feb 04 '11 at 14:19
10

This question has a similar issue.

WPF MVVM : Commands are easy. How to Connect View and ViewModel with RoutedEvent

The way I deal with this issue is to have a SelectedItem property in the ViewModel, and then bind the SelectedItem of your ListBox or whatever to that property.

Community
  • 1
  • 1
Cameron MacFarland
  • 70,676
  • 20
  • 104
  • 133
  • 1
    valid point, but not applicable to all scenarios... I have a scenario where I am doing what you suggest, but also need to use the SelectionChanged event – Jordan Jun 03 '13 at 19:03
9

Consider Microsoft.Xaml.Behaviors.Wpf, its owner is Microsoft which you can see in that page.

System.Windows.Interactivity.WPF owner is mthamil, anybody can tell me is it reliable ?

Example of Microsoft.Xaml.Behaviors.Wpf:

<UserControl ...
             xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors"
             ...>

<Button x:Name="button">
    <behaviors:Interaction.Triggers>
        <behaviors:EventTrigger EventName="Click" SourceObject="{Binding ElementName=button}">
            <behaviors:InvokeCommandAction Command="{Binding ClickCommand}" />
        </behaviors:EventTrigger>
    </behaviors:Interaction.Triggers>
</Button>

</UserControl>
huang
  • 919
  • 11
  • 22
  • 6
    System.Windows.Interactivity was maintained by the Micrososft Blend/Expression team, who in recent years open sourced and re-packaged it as the nuget Microsoft.Xaml.Behaviours.Wpf, check out [This blog](https://devblogs.microsoft.com/dotnet/open-sourcing-xaml-behaviors-for-wpf/) for more – Eddie Ted Crocombe Aug 09 '19 at 09:37
  • 1
    Correct nuget is Microsoft.Xaml.Behaviors – user487779 Oct 11 '20 at 11:24
8

I know it's a bit late but, Microsoft has made their Xaml.Behaviors open source and it's now much easier to use interactivity with just one namespace.

  1. First add Microsoft.Xaml.Behaviors.Wpf Nuget packge to your project.
    https://www.nuget.org/packages/Microsoft.Xaml.Behaviors.Wpf/
  2. add xmlns:behaviours="http://schemas.microsoft.com/xaml/behaviors" namespace to your xaml.

Then use it like this,

<Button Width="150" Style="{DynamicResource MaterialDesignRaisedDarkButton}">
   <behaviours:Interaction.Triggers>
       <behaviours:EventTrigger EventName="Click">
           <behaviours:InvokeCommandAction Command="{Binding OpenCommand}" PassEventArgsToCommand="True"/>
       </behaviours:EventTrigger>
    </behaviours:Interaction.Triggers>
    Open
</Button>

PassEventArgsToCommand="True" should be set as True and the RelayCommand that you implement can take RoutedEventArgs or objects as template. If you are using object as the parameter type just cast it to the appropriate event type.

The command will look something like this,

OpenCommand = new RelayCommand<object>(OnOpenClicked, (o) => { return true; });

The command method will look something like this,

private void OnOpenClicked(object parameter)
{
    Logger.Info(parameter?.GetType().Name);
}

The 'parameter' will be the Routed event object.

And the log incase you are curious,

2020-12-15 11:40:36.3600|INFO|MyApplication.ViewModels.MainWindowViewModel|RoutedEventArgs

As you can see the TypeName logged is RoutedEventArgs

RelayCommand impelmentation can be found here.

Why RelayCommand

PS : You can bind to any event of any control. Like Closing event of Window and you will get the corresponding events.

Snippy Valson
  • 231
  • 3
  • 9
7

Your best bet is using Windows.Interactivity. Use EventTriggers to attach an ICommand to any RoutedEvent.

Here is an article to get you started : Silverlight and WPF Behaviours and Triggers

decyclone
  • 30,394
  • 6
  • 63
  • 80
2
<ListBox SelectionChanged="{eb:EventBinding Command=SelectedItemChangedCommand, CommandParameter=$e}">

</ListBox>

Command

{eb:EventBinding} (Simple naming pattern to find Command)

{eb:EventBinding Command=CommandName}

CommandParameter

$e (EventAgrs)

$this or $this.Property

string

https://github.com/JonghoL/EventBindingMarkup

jongho
  • 79
  • 2
1

Sometimes solution of binding event to command through Interactivity trigger doesn't work, when it's needed to bind the event of custom usercontrol. In this case you can use custom behavior.

Declare binding behavior like:

public class PageChangedBehavior
{
    #region Attached property

    public static ICommand PageChangedCommand(DependencyObject obj)
    {
        return (ICommand)obj.GetValue(PageChangedCommandProperty);
    }
    public static void SetPageChangedCommand(DependencyObject obj, ICommand value)
    {
        obj.SetValue(PageChangedCommandProperty, value);
    }

    public static readonly DependencyProperty PageChangedCommandProperty =
        DependencyProperty.RegisterAttached("PageChangedCommand", typeof(ICommand), typeof(PageChangedBehavior),
            new PropertyMetadata(null, OnPageChanged));

    #endregion

    #region Attached property handler

    private static void OnPageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = d as PageControl;
        if (control != null)
        {
            if (e.NewValue != null)
            {
                control.PageChanged += PageControl_PageChanged;
            }
            else
            {
                control.PageChanged -= PageControl_PageChanged;
            }
        }
    }

    static void PageControl_PageChanged(object sender, int page)
    {
        ICommand command = PageChangedCommand(sender as DependencyObject);

        if (command != null)
        {
            command.Execute(page);
        }
    }

    #endregion

}

And then bind it to command in xaml:

        <controls:PageControl
            Grid.Row="2"
            CurrentPage="{Binding Path=UsersSearchModel.Page,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
            PerPage="{Binding Path=UsersSearchModel.PageSize,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
            Count="{Binding Path=UsersSearchModel.SearchResults.TotalItemCount}"
            behaviors:PageChangedBehavior.PageChangedCommand="{Binding PageChangedCommand}">
        </controls:PageControl>
Fragment
  • 1,555
  • 1
  • 26
  • 33
1

I would follow the top answer in this question

Basically your view model will contain a list of all your items and a list of selected items. You can then attach a behaviour to your listbox that manages your list of selected items.

Doing this means you having nothing in the code behind and the xaml is fairly easy to follow, also the behaviour can be re-used elsewhere in your app.

<ListBox ItemsSource="{Binding AllItems}" Demo:SelectedItems.Items="{Binding SelectedItems}" SelectionMode="Multiple" />
Community
  • 1
  • 1
Tom Dudfield
  • 3,077
  • 2
  • 25
  • 39
0

As @Cameron MacFarland mentions, I would simply two-way bind to a property on the viewModel. In the property setter you could do whatever logic you require, such as adding to a list of contacts, depending on your requirements.

However, i wouldn't necessarily call the property 'SelectedItem' as the viewModel shouldn't know about the view layer and how it's interacting with it's properties. I'd call it something like CurrentContact or something.

Obviously this is unless you just want to create commands as an exercise to practice etc.

HAdes
  • 16,713
  • 22
  • 58
  • 74
  • 1
    I disagree that the view model shouldn't know about the view layer. It is, after all, a model of the view. It shouldn't manipulate objects in the view, but that's only so that it can be instantiated independently of the view in unit tests. I wouldn't call the property `SelectedItem` unless the view model's collection were named `Items`, but that's a different issue. – Robert Rossney Feb 04 '11 at 16:38
  • See what you mean, but I tend to not perceive the vm as a "model of the view", more as an adapter which doesn't make any assumptions about the UI but simply exposes it's state and behaviour through commands and notifications. And this seperation is not "only" for unit tests, as you say, but also means that the UI can easily be switched in and out and modified as and when required without the need to modify the VM. – HAdes Feb 07 '11 at 10:08
0

This is an implementation using a MarkupExtension. Despite the low level nature (which is required in this scenario), the XAML code is very straight forward:

XAML

<SomeControl Click="{local:EventBinding EventToCommand}" CommandParameter="{local:Int32 12345}" />

Marup Extension

public class EventBindingExtension : MarkupExtension
{
    private static readonly MethodInfo EventHandlerImplMethod = typeof(EventBindingExtension).GetMethod(nameof(EventHandlerImpl), new[] { typeof(object), typeof(string) });
    public string Command { get; set; }

    public EventBindingExtension()
    {
    }
    public EventBindingExtension(string command) : this()
    {
        Command = command;
    }

    // Do not use!!
    public static void EventHandlerImpl(object sender, string commandName)
    {
        if (sender is FrameworkElement frameworkElement)
        {
            object dataContext = frameworkElement.DataContext;

            if (dataContext?.GetType().GetProperty(commandName)?.GetValue(dataContext) is ICommand command)
            {
                object commandParameter = (frameworkElement as ICommandSource)?.CommandParameter;
                if (command.CanExecute(commandParameter)) command.Execute(commandParameter);
            }
        }
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (serviceProvider.GetService(typeof(IProvideValueTarget)) is IProvideValueTarget targetProvider &&
            targetProvider.TargetObject is FrameworkElement targetObject &&
            targetProvider.TargetProperty is MemberInfo memberInfo)
        {
            Type eventHandlerType;
            if (memberInfo is EventInfo eventInfo) eventHandlerType = eventInfo.EventHandlerType;
            else if (memberInfo is MethodInfo methodInfo) eventHandlerType = methodInfo.GetParameters()[1].ParameterType;
            else return null;

            MethodInfo handler = eventHandlerType.GetMethod("Invoke");
            DynamicMethod method = new DynamicMethod("", handler.ReturnType, new[] { typeof(object), typeof(object) });

            ILGenerator ilGenerator = method.GetILGenerator();
            ilGenerator.Emit(OpCodes.Ldarg, 0);
            ilGenerator.Emit(OpCodes.Ldstr, Command);
            ilGenerator.Emit(OpCodes.Call, EventHandlerImplMethod);
            ilGenerator.Emit(OpCodes.Ret);

            return method.CreateDelegate(eventHandlerType);
        }
        else
        {
            throw new InvalidOperationException("Could not create event binding.");
        }
    }
}
bytecode77
  • 14,163
  • 30
  • 110
  • 141