简体   繁体   中英

WPF CommandParameter binding fails in ToolBar?

Here's the situation: It's a bit hard to describe, so skip to the steps to recreate and copy/paste the code into a new project if you want. ListViewModel contains a list of ViewModel (Items) and a list of ICommand (Actions). MainWindow has a ToolBar that binds to the Actions, a ListView that binds to the Items, and a ContextMenu that binds to the actions within the ListView. In my implementation of ICommand (Command.cs), I have added the ability to insert custom code (the OnIsVisible property) that checks if the command Visibility should be Visible or Collapsed. This code works great for the Actions in the ToolBar and ContextMenu until you open the ContextMenu. Then the CommandParameter for the ToolBar is forever null, except when the ContextMenu is open.

Steps to recreate:

  1. Select an item in the ListView
  2. Click "Show When Selected" in the ContextMenu
  3. Select another item in the ListView

At this point, the CommandParameter binding will always be NULL to the command object. So the "Show When Selected" button in the ToolBar will no longer appear.

Code:

In a new WPF application project called "NullParameter", create/edit the following files...

MainWindow.xaml:

<Window x:Class="NullParameter.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <Style TargetType="{x:Type ListViewItem}">
            <Setter Property="IsSelected"
                    Value="{Binding IsSelected}" />
        </Style>
        <Style x:Key="contextMenuItemStyle"
               TargetType="{x:Type MenuItem}">
            <Setter Property="Header"
                    Value="{Binding Header}" />
            <Setter Property="Command"
                    Value="{Binding}" />
            <Setter Property="Visibility"
                    Value="{Binding Visibility}" />
            <Setter Property="CommandParameter"
                    Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.Tag.SelectedItems}" />
        </Style>
        <DataTemplate x:Key="toolBarActionItemTemplate">
            <Button Content="{Binding Header}"
                    Command="{Binding}"
                    CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ToolBar}, Path=Tag.SelectedItems}"
                    Visibility="{Binding Visibility}" />
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.Children>
            <ToolBar Grid.Row="0"
                     ItemsSource="{Binding Actions}"
                     ItemTemplate="{StaticResource toolBarActionItemTemplate}"
                     Tag="{Binding}" />
            <ListView Grid.Row="1"
                      ItemsSource="{Binding Items}"
                      SelectionMode="Extended"
                      Tag="{Binding}">
                <ListView.ContextMenu>
                    <ContextMenu ItemsSource="{Binding Actions}"
                                 ItemContainerStyle="{StaticResource contextMenuItemStyle}" />
                </ListView.ContextMenu>
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="Id"
                                        DisplayMemberBinding="{Binding Id}"/>
                    </GridView>
                </ListView.View>
            </ListView>
        </Grid.Children>
    </Grid>

</Window>

CommandBase.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;

namespace NullParameter
{
    public abstract class CommandBase : ICommand, INotifyPropertyChanged
    {
        private Visibility _visibility;
        private string _error;

        public Visibility Visibility
        {
            get { return _visibility; }
            protected set
            {
                if (_visibility == value)
                    return;

                _visibility = value;

                if (PropertyChanged == null)
                    return;

                PropertyChanged(this, new PropertyChangedEventArgs("Visibility"));
            }
        }
        public string Error
        {
            get { return _error; }
            set
            {
                if (_error == value)
                    return;

                _error = value;

                if (PropertyChanged == null)
                    return;

                PropertyChanged(this, new PropertyChangedEventArgs("Error"));
            }
        }

        public bool CanExecute(object parameter)
        {
            Error = DoCanExecute(parameter);

            Visibility = DoIsVisible(parameter) ? Visibility.Visible : Visibility.Collapsed;

            return Error == null;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            DoExecute(parameter);
        }

        protected abstract string DoCanExecute(object parameter);
        protected abstract bool DoIsVisible(object parameter);
        protected abstract void DoExecute(object parameter);

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Command.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;

namespace NullParameter
{
    public class Command : CommandBase
    {
        public Action OnExecute { get; set; }
        public Func<bool> OnIsVisible { get; set; }
        public Func<string> OnCanExecute { get; set; }
        public string Header { get; set; }

        protected override string DoCanExecute(object parameter)
        {
            if (OnCanExecute == null)
                return null;

            return OnCanExecute();
        }

        protected override bool DoIsVisible(object parameter)
        {
            if (OnIsVisible == null)
                return true;

            return OnIsVisible();
        }

        protected override void DoExecute(object parameter)
        {
            if (OnExecute == null)
                return;

            OnExecute();
        }
    }

    public class Command<T> : CommandBase
        where T : class
    {
        public Action<T> OnExecute { get; set; }
        public Func<T, bool> OnIsVisible { get; set; }
        public Func<T, string> OnCanExecute { get; set; }
        public string Header { get; set; }

        protected T Convert(object parameter)
        {
            if (parameter == null)
                Console.WriteLine("NULL");

            return parameter as T;
        }

        protected override string DoCanExecute(object parameter)
        {
            if (OnCanExecute == null)
                return null;

            var p = Convert(parameter);
            if (p == null)
                return "Invalid Parameter";

            return OnCanExecute(p);
        }

        protected override bool DoIsVisible(object parameter)
        {
            if (OnIsVisible == null)
                return true;

            var p = Convert(parameter);
            if (p == null)
                return false;

            return OnIsVisible(p);
        }
        protected override void DoExecute(object parameter)
        {
            if (OnExecute == null)
                return;

            var p = Convert(parameter);
            if (p == null)
                return;

            OnExecute(p);
        }
    }
}

ListViewModel.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace NullParameter
{
    public class ListViewModel
    {
        public IList<ViewModel> Items { get; private set; }
        public IList SelectedItems { get; private set; }
        public IList<ICommand> Actions { get; private set; }

        public ListViewModel()
        {
            var items = new ObservableCollection<ViewModel>()
            {
                new ViewModel()
                {
                    Id = 1
                },
                new ViewModel()
                {
                    Id = 2
                },
                new ViewModel()
                {
                    Id = 3
                },
            };
            Items = items;
            SelectedItems = items;
            Actions = new List<ICommand>()
            {
                new Command()
                {
                    OnExecute = ShowAlways,
                    Header = "Show Always"
                },
                new Command<IList<ViewModel>>()
                {
                    OnExecute = ShowWhenSelected,
                    OnIsVisible = (list) => { return list.Count(o => o.IsSelected) > 0; },
                    Header = "Show When Selected"
                }
            };
        }

        public void ShowAlways()
        {
            Console.WriteLine("ShowAlways()");
        }

        public void ShowWhenSelected(IList<ViewModel> viewModels)
        {
            Console.WriteLine("ShowWhenSelected({0})", String.Join(",", viewModels.Where(o => o.IsSelected).Select(o => o.Id)));
        }
    }
}

ViewModel.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NullParameter
{
    public class ViewModel : INotifyPropertyChanged
    {
        private bool _isSelected;
        private int _id;

        public event PropertyChangedEventHandler PropertyChanged;

        public int Id
        {
            get { return _id; }
            set
            {
                if (_id == value)
                    return;

                _id = value;

                if (PropertyChanged == null)
                    return;

                PropertyChanged(this, new PropertyChangedEventArgs("Id"));
            }
        }
        public bool IsSelected
        {
            get { return _isSelected; }
            set
            {
                if (_isSelected == value)
                    return;

                _isSelected = value;

                if (PropertyChanged == null)
                    return;

                PropertyChanged(this, new PropertyChangedEventArgs("IsSelected"));
            }
        }
    }
}

So after reading a few posts about how glitchy the CommandParameter DependencyProperty is, I've given up on using it entirely. Instead, I simply construct my Command objects by passing in the list of selected items in my ListViewModel. Then in the CanExecute and Execute method, I use the stored list of selected items instead of the .NET supplied parameter.

Although this provides a workable solution, it does not necessarily solve the problem posed by the initial question. So I will leave this here as a suggestion for anyone else unfortunate enough to have these issues.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM