-1

I've been practicing MVVM pattern and come across the problem which I don't know how to solve. The problem is pretty simple and I hope the solution as well. The point is that I'm trying to use a command and binding for an element, when I'm setting up it's style, but I can't do it at the same time.

I have the following style for ListBoxItem:

<Style x:Key="OptionDieStyle" TargetType="ListBoxItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ListBoxItem">
                        <Border Width="Auto"
                                BorderThickness="1.5"
                                CornerRadius="10"
                                Height="30"
                                Background="Transparent"
                                Margin="5">
                            <TextBlock Margin="5"
                                       Text="{Binding}"
                                       Foreground="White"
                                       VerticalAlignment="Center"/>
                            <Border.InputBindings>
                                <MouseBinding MouseAction="LeftClick" Command="#Omitted"
                            </Border.InputBindings>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

This ListBox is filled with strings which are displayed in particular way because of the style. That means that when I want to handle user's click on that element, using command, I need to set DataContext, which contains ViewModel, where command is located, for this item, but if I do it no content will be displayed in ListBox Items. Certainly, I could set event for this Border like "MouseDown" but it would be the wrong way to use MVVM.

If you have some thoughts how to solve this using commands please share them.

yegor
  • 65
  • 5
  • https://stackoverflow.com/a/1026407/1136211. Also better use a Button, which already provides a Command property. – Clemens Jan 26 '23 at 17:34
  • With a listbox you select the item you click, so you could drive processing with that selection. – Andy Jan 26 '23 at 18:08
  • In such cases, I usually use RoutedCommand in the list elements and their processing (CommandBinding) at the Window level. – EldHasp Jan 26 '23 at 19:33

2 Answers2

1

To make these scenarios easier, I've derived a class from CommandBindin. In which he added the ability to bind to ViewModel commands. You can set the binding to both Execute and PreviewExecute.

using System;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;

namespace CommonCore.AttachedProperties
{
    public class CommandBindingHelper : CommandBinding
    {
        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        protected static readonly DependencyProperty CommandProperty =
            DependencyProperty.RegisterAttached(
                "Command",
                typeof(ICommand),
                typeof(CommandBindingHelper),
                new PropertyMetadata(null));

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        protected static readonly DependencyProperty PreviewCommandProperty =
            DependencyProperty.RegisterAttached(
                "PreviewCommand",
                typeof(ICommand),
                typeof(CommandBindingHelper),
                new PropertyMetadata(null));

        public BindingBase Binding { get; set; }
        public BindingBase PreviewBinding { get; set; }

        public CommandBindingHelper()
        {
            Executed += (s, e) => PrivateExecuted(CheckSender(s), e.Parameter, CommandProperty, Binding);
            CanExecute += (s, e) => e.CanExecute = PrivateCanExecute(CheckSender(s), e.Parameter, CommandProperty, Binding);
            PreviewExecuted += (s, e) => PrivateExecuted(CheckSender(s), e.Parameter, PreviewCommandProperty, PreviewBinding);
            PreviewCanExecute += (s, e) => e.CanExecute = PrivateCanExecute(CheckSender(s), e.Parameter, PreviewCommandProperty, PreviewBinding);
        }
        private static void PrivateExecuted(UIElement sender, object parameter, DependencyProperty commandProp, BindingBase commandBinding)
        {
            ICommand command = GetCommand(sender, commandProp, commandBinding);
            if (command is not null && command.CanExecute(parameter))
            {
                command.Execute(parameter);
            }
        }
        private static bool PrivateCanExecute(UIElement sender, object parameter, DependencyProperty commandProp, BindingBase commandBinding)
        {
            ICommand command = GetCommand(sender, commandProp, commandBinding);
            return command?.CanExecute(parameter) ?? true;
        }

        private static UIElement CheckSender(object sender)
        {
            if (sender is not UIElement element)
                throw new NotImplementedException("Implemented only for UIElement.");
            return element;
        }
        private static ICommand GetCommand(UIElement sender, DependencyProperty commandProp, BindingBase commandBinding)
        {
            BindingBase binding = BindingOperations.GetBindingBase(sender, commandProp);
            if (binding != commandBinding)
            {
                if (commandBinding is null)
                {
                    BindingOperations.ClearBinding(sender, commandProp);
                }
                else
                {
                    BindingOperations.SetBinding(sender, commandProp, commandBinding);
                }
            }
            return (ICommand)sender.GetValue(CommandProperty);
        }
    }
}

An example of its use:

using Simplified; // This is the space of my ViewModelBase implementation
using System.Collections.ObjectModel;

namespace Core2023.SO.ASTERY.CommandInListItem
{
    public class ListItemsViewModel : ViewModelBase
    {
        public ObservableCollection<string> Items { get; } = new("first second third fourth fifth".Split());
        public RelayCommand RemoveCommand => GetCommand<string>(item => Items.Remove(item));
    }
}
<Window x:Class="Core2023.SO.ASTERY.CommandInListItem.ListItemsWindow"
        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:Core2023.SO.ASTERY.CommandInListItem"
        xmlns:ap="clr-namespace:CommonCore.AttachedProperties;assembly=CommonCore"
        mc:Ignorable="d"
        Title="ListItemsWindow" Height="450" Width="800"
        FontSize="20">
    <Window.DataContext>
        <local:ListItemsViewModel/>
    </Window.DataContext>
    <Window.CommandBindings>
        <ap:CommandBindingHelper Command="Delete" Binding="{Binding RemoveCommand}"/>
    </Window.CommandBindings>
    <Grid>
        <ItemsControl ItemsSource="{Binding Items}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <UniformGrid Rows="1" Margin="5">
                        <TextBlock Text="{Binding}"/>
                        <Button Content="Remove"
                                Command="Delete"
                                CommandParameter="{Binding}"/>
                    </UniformGrid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

What do i do if I want to set different name?

  1. The easiest way is to create a command in Window or (better) App resources.
    <Application.Resources>
        <RoutedUICommand x:Key="commands.Remove" Text="Delete Item" />
    </Application.Resources>
   <Button Content="Remove"
           Command="{StaticResource commands.Remove}"
           CommandParameter="{Binding}"/>
  1. Create a static property containing the command. But it should be created at the View level, not the ViewModel.
    public static class MyCommands
    {
        public static RoutedUICommand Remove { get; }
            = new RoutedUICommand("Delete Item", "Remove", typeof(MyCommands));
        public static RoutedUICommand Add { get; }
            = new RoutedUICommand("Add Item", "Add", typeof(MyCommands));
    }
   <Button Content="Remove"
           Command="{x:Static local:MyCommands.Remove}"
           CommandParameter="{Binding}"/>
  1. Adding a markup extension to the previous version to make it easier to use in XAML.
    public class MyCommandsExtension : MarkupExtension
    {
        public string? CommandName { get; set; }

        public MyCommandsExtension() { }

        public MyCommandsExtension(string commandName) => CommandName = commandName;

        public override object ProvideValue(IServiceProvider serviceProvider)
            => CommandName switch
            {
                nameof(MyCommands.Remove) => MyCommands.Remove,
                nameof(MyCommands.Add) => MyCommands.Add,
                _ => throw new NotImplementedException()
            };
    }
   <Button Content="Remove"
           Command="{local:MyCommands Remove}"
           CommandParameter="{Binding}"/>
EldHasp
  • 6,079
  • 2
  • 9
  • 24
0

The approach above is working fine, but only if we're going to use commands with default ApplicationCommands' names and won't give them individual names. I was racking my brains and eventually found the proper approach. All I had to do is just make my command static in ViewModel and change definition for my command in XAML like this:

Command="{x:Static viewModels:MyViewModel.MyCommand}

yegor
  • 65
  • 5