namg_engr
namg_engr

Reputation: 359

WPF Aggregating Commands using Multibinding in MVVM

I'm examining the solution of Leonid's command aggregating using multibinding in MVVM. https://www.codeproject.com/Articles/990113/MultiBinding-for-WPF-Command-Combining?msg=5666640#xx5666640xx I'm having issues with this solution when using it in a menu. I've added a simple menu with two items: item1 and item2. If menuitem item1 is selected then both menuitems item1 and item2 fire which is not what I want. Another aspect which I don't understand is if the XAML section which contains <Button.Command> is NOT commented then all four North, West, South and East commands fire when selecting menuitem item1. The multibinding for the button doesn't seem to be tied strictly to the button and is available to other controls. Any thoughts?

I'm aware of another command aggregate solution by Josh Smith but I read that his solution doesn't completely fit the MVVM context.

MainWindow XAML

<Window x:Class="MultiCommandButtonNoParams.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ice="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options" 
    xmlns:vm="clr-namespace:MultiCommandButtonNoParams.ViewModels"
    xmlns:bc="clr-namespace:MultiCommandButtonNoParams.BindingConverters"
    Title="MultiCommandButtonNoParams" Height="350" Width="525">
<Window.Resources>
    <vm:ViewModel x:Key="viewModel" />
    <bc:MultiCommandConverter x:Key="multiCommandConverter"/>
</Window.Resources>
<Window.DataContext>
    <StaticResource ResourceKey="viewModel"/>
</Window.DataContext>

<Grid ShowGridLines="False" Background="Ivory">
    <Grid.RowDefinitions>
        <RowDefinition Height="0.5*"></RowDefinition>
        <RowDefinition Height="3*"></RowDefinition>
        <RowDefinition Height="*"></RowDefinition>
    </Grid.RowDefinitions>

    <StackPanel Grid.Row="0">
        <Menu Focusable="False">
            <MenuItem Header="Menu">
                <MenuItem Header="item1">
                    <MenuItem.Command>
                        <MultiBinding Converter="{StaticResource multiCommandConverter}" >
                            <Binding Path="NorthActionCommand"/>
                        </MultiBinding>
                    </MenuItem.Command>
                </MenuItem>
                <MenuItem Header="item2">
                    <MenuItem.Command>
                        <MultiBinding Converter="{StaticResource multiCommandConverter}" >
                            <Binding Path="WestActionCommand"/>
                        </MultiBinding>
                    </MenuItem.Command>
                </MenuItem>
            </MenuItem>
        </Menu>
    </StackPanel>

    <StackPanel Grid.Row="1">
        <TextBlock VerticalAlignment="Top" TextAlignment="Center" TextWrapping="Wrap" LineHeight="53">
            <TextBlock.Inlines>
                <Run Text="{Binding Path=NorthCommandManifest}" FontWeight="Bold" FontSize="18" Style="{StaticResource runForeground}" /> 
                <LineBreak/>
                <Run Text="{Binding Path=WestCommandManifest}" FontWeight="Heavy" FontSize="18" FontStyle="Italic" Style="{StaticResource runForeground}"/>
                <LineBreak/>
                <Run Text="{Binding Path=SouthCommandManifest}" FontWeight="ExtraBold" FontSize="18" FontStyle="Oblique" Style="{StaticResource runForeground}"/>
                <LineBreak/>
                <Run  Text="{Binding Path=EastCommandManifest}" FontWeight="DemiBold" FontSize="18" FontStyle="Normal" Style="{StaticResource runForeground}"/>            
            </TextBlock.Inlines>
        </TextBlock>
    </StackPanel>
    <Grid Grid.Row="2">
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Button Grid.Column="1" Margin="10,2,10,2" Style="{StaticResource buttonBackground}" Focusable="False">
            <!--<Button.Command>

                Multicommand construction that consists of a set of sequentially executed commands.
                Each command sends a message about execution to the TextBlock defined above.

                <MultiBinding Converter="{StaticResource multiCommandConverter}" >
                    <Binding Path="NorthActionCommand"/>
                    <Binding Path="WestActionCommand"/>
                    <Binding Path="SouthActionCommand"/>
                    <Binding Path="EastActionCommand"/>
                </MultiBinding>
            </Button.Command>-->
            <TextBlock FontWeight="Heavy" FontSize="18" TextAlignment="Center" TextWrapping="Wrap" Style="{StaticResource buttonForeground}">
                Multi Command Button
            </TextBlock>
        </Button>
    </Grid>
</Grid>

MultiCommandConverter.cs

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows.Data;
using MultiCommandButtonNoParams.Commands;

namespace MultiCommandButtonNoParams.BindingConverters
{
    public class MultiCommandConverter : IMultiValueConverter
    {
        private List<object> _value = new List<object>( );

        /// <summary>
        /// dobbin of the converter
        /// </summary>
        /// <param name="value">commands binded by means of multibiniding</param>
        /// <returns>compound Relay command</returns>
        public object Convert( object[ ] value, Type targetType,
            object parameter, CultureInfo culture )
        {
            _value.AddRange( value );
            return new RelayCommand( GetCompoundExecute( ), GetCompoundCanExecute( ) );
        }

        /// <summary>
        /// here - mandatory duty
        /// </summary>
        public object[ ] ConvertBack( object value, Type[ ] targetTypes,
            object parameter, CultureInfo culture )
        {
            return null;
        }

        /// <summary>
        /// for execution of all commands
        /// </summary>
        /// <returns>Action<object> that plays a role of the joint Execute</returns>
        private Action<object> GetCompoundExecute( )
        {
            return ( parameter ) =>
            {
                foreach ( RelayCommand command in _value )
                {
                    if ( command != default( RelayCommand ) )
                        command.Execute( parameter );
                }
            };
        }

        /// <summary>
        /// for check if execution of all commands is possible
        /// </summary>
        /// <returns>Predicate<object> that plays a role of the joint CanExecute</returns>
        private Predicate<object> GetCompoundCanExecute( )
        {
            return ( parameter ) =>
            {
                bool res = true;
                foreach ( RelayCommand command in _value )
                    if ( command != default( RelayCommand ) )
                        res &= command.CanExecute( parameter );
                return res;
            };
        }
    }
}

Upvotes: 1

Views: 425

Answers (1)

Peter Duniho
Peter Duniho

Reputation: 70661

I would just like to know why all commands in 'MenuItem` fire when only one is selected

It's because your implementation of MultiCommandConverter is flawed:

public object Convert( object[ ] value, Type targetType,
    object parameter, CultureInfo culture )
{
    _value.AddRange( value );
    return new RelayCommand( GetCompoundExecute( ), GetCompoundCanExecute( ) );
}

Every time the converter object is asked to convert the input bindings to a new ICommand object, it will add the multiple values passed to it to its internal list of commands and then return a new RelayCommand() object that simply calls delegates that refer to those commands. I.e. because the delegate instances returned by GetCompoundExecute() and GetCompoundCanExecute() capture the _value field, changes that occur later to the list referred to by that field are reflected in delegates that were created earlier.

Then, you create this converter as a resource, without specifying x:Shared=false. This means every place you used the converter is using the same object. So by the time all of the XAML is parsed, you have one converter that combines all of the commands used for all places where you've used that converter.

One fix might be to go ahead and specific x:Shared=false in the resource. But IMHO that's not really a good way to do it. For one, it means that now you've got a trap in your converter and have to remember that you need to specify that any time you put the converter in a resource dictionary. For another, you have another bug in the converter, where you add the values to the list every time the Convert() method is called. Which means that if you try to bind to values that may get updated from time to time, the list will get longer and longer, and will never eliminate the old values.

You could fix this by resetting the list every time the Convert() method is called, i.e. call _value.Clear(). But it's my opinion that that's still a design flaw. A converter should convert. It should not itself play a part in the result, as your implementation does here.

Alternatives that IMHO would be preferable include:

  • Writing a CompoundRelayCommand() that incorporates the GetCompoundExecute() and GetCompoundCanExecute() functionality, which you create a new instance of every time the Convert() method is called.
  • Aggregate the input values in the Convert() method itself by creating a new multicast delegate that chains the input commands, and then pass that multicast delegate to the RelayCommand().

Either of those approaches would be much better than just updating what you have now by fixing the "list isn't cleared" bug and using x:Shared="false" in the resource dictionary.

Upvotes: 1

Related Questions