Агрегирующие команды WPF, использующие мультисвязывание в MVVM - PullRequest
1 голос
/ 01 октября 2019

Я изучаю решение агрегирования команд Леонида с использованием мультисвязывания в MVVM. https://www.codeproject.com/Articles/990113/MultiBinding-for-WPF-Command-Combining?msg=5666640#xx5666640xx У меня проблемы с этим решением при использовании его в меню. Я добавил простое меню с двумя пунктами: item1 и item2. Если пункт меню item1 выбран, то оба пункта menu1 и item2 срабатывают, что не то, что я хочу. Еще один аспект, который я не понимаю, - если раздел XAML, который содержит <Button.Command>, НЕ комментируется, тогда все четыре команды «Север», «Запад», «Юг» и «Восток» запускаются при выборе пункта меню menu1Мультисвязывание для кнопки, похоже, не привязано строго к кнопке и доступно для других элементов управления. есть идеи?

Мне известно о другом решении для агрегирования команд от Джоша Смита, но я читал, что его решение не полностью соответствует контексту MVVM.

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

1 Ответ

1 голос
/ 02 октября 2019

Я просто хотел бы знать, почему все команды в «MenuItem» запускаются, когда выбрана только одна

Это потому, что ваша реализация MultiCommandConverter имеет недостатки:

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

Каждый раз, когда объекту конвертера предлагается преобразовать входные привязки в новый объект ICommand, он добавляет несколько переданных ему значений в свой внутренний список команд и затем возвращает новый объект RelayCommand(), который простовызывает делегатов, которые ссылаются на эти команды. То есть, поскольку экземпляры делегатов, возвращаемые GetCompoundExecute() и GetCompoundCanExecute(), захватывают поле _value, изменения, которые происходят позже в списке, на который ссылается это поле, отражаются в делегатах, которые были созданы ранее.

Затем,Вы создаете этот конвертер как ресурс, не указывая x:Shared=false. Это означает, что в каждом месте, где вы использовали конвертер, используется один и тот же объект. Таким образом, ко времени разбора всего XAML у вас есть один конвертер, который объединяет все команды, используемые для всех мест, где вы использовали этот конвертер. * в ресурсе. Но ИМХО, это не очень хороший способ сделать это. Во-первых, это означает, что теперь у вас есть ловушка в вашем конвертере, и вы должны помнить, что вам нужно указывать это каждый раз, когда вы помещаете конвертер в словарь ресурсов. С другой стороны, у вас есть другая ошибка в конвертере, когда вы добавляете значения в список каждый раз, когда вызывается метод Convert(). Это означает, что если вы попытаетесь привязать значения, которые могут время от времени обновляться, список будет становиться все длиннее и длиннее и никогда не удалит старые значения.

Это можно исправить, сбрасывая список каждыевремя вызова метода Convert(), то есть вызова _value.Clear(). Но я считаю, что это все еще недостаток дизайна. Конвертер должен конвертировать . Сам по себе он не должен играть роль в результате, как ваша реализация делает это.

Альтернативы, которые IMHO были бы предпочтительнее, включают:

  • Написание CompoundRelayCommand(), включающее * 1033Функциональные возможности * и GetCompoundCanExecute(), которые вы создаете каждый раз при каждом вызове метода Convert().
  • Объедините входные значения в самом методе Convert(), создав новый многоадресный делегат, который объединяет цепочкивведите команды, а затем передайте этот многоадресный делегат RelayCommand().

. Любой из этих подходов будет гораздо лучше, чем просто обновить то, что у вас есть, исправив ошибку «список не очищен». и используя x:Shared="false" в словаре ресурсов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...