开发者

WPF Binding UI events to commands in ViewModel

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;
    }
}


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".


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.


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.


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>


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


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.


<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


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" />


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>


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.


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.");
        }
    }
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜