【问题标题】:WPF Aggregating Commands using Multibinding in MVVM在 MVVM 中使用多重绑定的 WPF 聚合命令
【发布时间】:2019-10-01 05:08:22
【问题描述】:

我正在研究使用 MVVM 中的多重绑定来聚合 Leonid 的命令的解决方案。 https://www.codeproject.com/Articles/990113/MultiBinding-for-WPF-Command-Combining?msg=5666640#xx5666640xx 在菜单中使用此解决方案时,我遇到了问题。我添加了一个包含两个项目的简单菜单:item1 和 item2。如果选择了 menuitem item1,则 menuitem item1 和 item2 都会触发,这不是我想要的。我不理解的另一个方面是在选择MenuItem Item 1时,如果没有评论<Button.Command>的XAML部分,那么<Button.Command>尚未评论所有四个北,西,南和东部指挥火。按钮的多重绑定似乎并不严格绑定到按钮,并且可用于其他控件。有什么想法吗?

我知道 Josh Smith 的另一个命令聚合解决方案,但我读到他的解决方案并不完全适合 MVVM 上下文。

主窗口 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;
            };
        }
    }
}

【问题讨论】:

  • 我将常用功能分解为方法,然后由任何需要它们的命令调用。在极少数情况下,我想要更复杂的功能,我会使用任务列表。将命令的功能分解为一个任务,每个命令一个。如果我有一些其他命令应该运行其中的几个,我会在这些任务的列表中执行 task.whenall。 Plinq 如果我需要按顺序运行它们。虽然因为我有这样的要求,但现在我可能会使用异步流来按顺序运行任务。
  • MultiBinding 用于触发多个命令。如果您不想这样做,为什么需要MultiBinding?为什么不能只将Command 属性绑定到单个命令?
  • @mm8 在某些情况下,我只需要一个命令。在其他情况下需要依次触发多个命令并且需要使用MultiBinding
  • @namg_engr:所以当您需要触发单个命令时不要使用MultiBinding,反之亦然?
  • @mm8 是的。好点子。我的错。我只想知道为什么“MenuItem”中的所有命令在只选择一个时都会触发。

标签: wpf mvvm


【解决方案1】:

我想知道为什么“MenuItem”中的所有命令都在只选择一个时触发

这是因为您对MultiCommandConverter 的实现存在缺陷:

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

每次要求转换器对象将输入绑定转换为新的ICommand 对象时,它会将传递给它的多个值添加到其内部命令列表中,然后返回一个新的RelayCommand() 对象,该对象只需调用引用这些命令的委托。 IE。因为GetCompoundExecute()GetCompoundCanExecute() 返回的委托实例捕获了_value 字段,所以稍后对该字段引用的列表发生的更改会反映在之前创建的委托中。

然后,您将此转换器创建为资源,而不指定 x:Shared=false。这意味着您使用转换器的每个地方都使用相同的对象。因此,当所有 XAML 都被解析时,您就拥有了一个转换器,它结合了您使用过该转换器的所有地方所使用的所有命令。

一种解决方法可能是继续在资源中使用特定的x:Shared=false。但恕我直言,这并不是一个真正的好方法。一方面,这意味着现在您的转换器中有一个陷阱,并且必须记住,每次将转换器放入资源字典时都需要指定它。另一方面,您在转换器中有另一个错误,每次调用 Convert() 方法时都会将值添加到列表中。这意味着,如果您尝试绑定到可能会不时更新的值,列表将变得越来越长,并且永远不会消除旧值。

您可以通过在每次调用 Convert() 方法时重置列表来解决此问题,即调用 _value.Clear()。但我认为这仍然是一个设计缺陷。转换器应该转换。它本身不应该像您的实现那样在结果中发挥作用。

恕我直言,更可取的替代方案包括:

  • 编写包含GetCompoundExecute()GetCompoundCanExecute() 功能的CompoundRelayCommand(),每次调用Convert() 方法时都会创建一个新实例。
  • 通过创建链接输入命令的新多播委托来聚合 Convert() 方法本身中的输入值,然后将该多播委托传递给 RelayCommand()

这些方法中的任何一种都比通过修复“列表未清除”错误并在资源字典中使用 x:Shared="false" 来更新您现在拥有的内容要好得多。

【讨论】:

  • 超级!谢谢你的解释和建议。我将着手研究第二种选择。
猜你喜欢
  • 2013-04-25
  • 1970-01-01
  • 2010-11-06
  • 2015-06-07
  • 2011-04-04
  • 1970-01-01
  • 1970-01-01
  • 2018-09-20
相关资源
最近更新 更多