【问题标题】:WPF - two commands on Listbox itemWPF - 列表框项上的两个命令
【发布时间】:2016-12-03 12:46:14
【问题描述】:

我正在为以前在 VS 插件中的功能开发一个 VS 包扩展。

插件会将文件加载到工具栏窗口中,然后如果用户双击一个项目(这是文件的名称),该文件将在 VS 的编辑器中打开。如果用户右键单击一个项目,它会给出一个弹出菜单。所以我的问题是将列表框项的这些操作(双击和右键单击)与我现有的代码连接起来的最佳方法是什么。

对于我们使用 WPF 的扩展,但对于插件,它是 windows 窗体。 但是,我对 WPF 不是很熟悉。大约一年前,我观看了 Brian Noyes 的 Pluralsight 课程“WPF MVVM In Depth”并在扩展中实现了一些东西,但是今年大部分时间我都没有在扩展上工作。结果是我对我写的代码只有模糊的回忆,我有点困惑最好的设计是什么。

那么让我告诉你我已经拥有的东西:

这是 XAML 文件:

<UserControl x:Class="Sym.VisualStudioExtension.Engines.TAEngineView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:behaviours="clr-namespace:Sym.VisualStudioExtension"
         xmlns:local="clr-namespace:Sym.VisualStudioExtension"
         local:ViewModelLocator.AutoWireViewModel="True"
         mc:Ignorable="d" 
         d:DesignHeight="700" d:DesignWidth="400">
<Grid>
    <TabControl x:Name="tabControl" HorizontalAlignment="Left" Height="490" Margin="19,44,-36,-234" VerticalAlignment="Top" Width="317">
        <TabItem Header="Parameter Files">
            <ListBox Margin="20" ItemsSource="{Binding ParameterFilesList}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Path=Name}" />
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>                    
            </ListBox>
        </TabItem>
        <TabItem Header="Calc Files">
            <ListBox Margin="20" ItemsSource="{Binding CalcFilesList}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Path=Name}" />
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>                   
            </ListBox>
        </TabItem>
    </TabControl>
    <Label x:Name="label" Content="{Binding Path=Title}" HorizontalAlignment="Left" Margin="19,13,0,0" VerticalAlignment="Top" Width="367
           " BorderThickness="2"/>

</Grid>

CalcFilesList 的类型为ObservableCollection&lt;CalcFile&gt;,ParameterFilesList 的类型为ObservableCollection&lt;Parameter&gt;

那我已经有了这个 RelayCommand 类:

using System;
using System.Diagnostics;
using System.Windows.Input;

namespace Sym.VisualStudioExtension
{
/// <summary>
/// A command whose sole purpose is to 
/// relay its functionality to other
/// objects by invoking delegates. The
/// default return value for the CanExecute
/// method is 'true'.
/// </summary>
public class RelayCommand : ICommand
{
    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;        

    #endregion // Fields

    #region Constructors

    /// <summary>
    /// Creates a new command that can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }

    /// <summary>
    /// Creates a new command.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;           
    }

    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameters)
    {
        return _canExecute == null ? true : _canExecute(parameters);
    }

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

    public void Execute(object parameters)
    {
        _execute(parameters);
    }

    #endregion // ICommand Members
}

public class RelayCommand<T> : ICommand
    {
        #region Fields

        private readonly Action<T> _execute = null;
        private readonly Predicate<T> _canExecute = null;

        #endregion

        #region Constructors

        /// <summary>
        /// Creates a new command that can always execute.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        public RelayCommand(Action<T> execute)
            : this(execute, null)
        {
        }

        /// <summary>
        /// Creates a new command with conditional execution.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        /// <param name="canExecute">The execution status logic.</param>
        public RelayCommand(Action<T> execute, Predicate<T> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            _execute = execute;
            _canExecute = canExecute;
        }

        #endregion

        #region ICommand Members

        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute((T)parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (_canExecute != null)
                    CommandManager.RequerySuggested += value;
            }
            remove
            {
                if (_canExecute != null)
                    CommandManager.RequerySuggested -= value;
            }
        }

        public void Execute(object parameter)
        {
            _execute((T)parameter);
        }

        #endregion
    }
}

还有这个 BindableBase 类:

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

namespace Sym.VisualStudioExtension
{
    public class BindableBase : INotifyPropertyChanged
    {
        protected virtual void SetProperty<T>(ref T member, T val, [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(member, val)) return;

            member = val;
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public event PropertyChangedEventHandler PropertyChanged = delegate { };
        protected virtual void OnPropertyChanged(string propertyName)
        {         
                PropertyChanged(this, new     PropertyChangedEventArgs(propertyName));     
        }

    }
}

这里是 ViewModelLocator:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Practices.Unity;
using Symplexity.VisualStudioExtension.Engines;

namespace Sym.VisualStudioExtension
{
    public static class ViewModelLocator
    {
        public static bool GetAutoWireViewModel(DependencyObject obj)
        {
            return (bool)obj.GetValue(AutoWireViewModelProperty);
        }

        public static void SetAutoWireViewModel(DependencyObject obj, bool value)
        {
            obj.SetValue(AutoWireViewModelProperty, value);
        }

        // Using a DependencyProperty as the backing store for AutoWireViewModel.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AutoWireViewModelProperty =
            DependencyProperty.RegisterAttached("AutoWireViewModel", typeof(bool), typeof(ViewModelLocator), new PropertyMetadata(false, AutoWireViewModelChanged));

        private static void AutoWireViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (DesignerProperties.GetIsInDesignMode(d)) return;
            var viewType = d.GetType();
            var viewTypeName = viewType.FullName;
            var viewModelTypeName = viewTypeName + "Model";
            var viewModelType = Type.GetType(viewModelTypeName);


            if (viewModelTypeName.Contains("UtilitiesViewModel"))
            {
                 UtilitiesViewModel uViewModel = ContainerHelper.Container.Resolve<UtilitiesViewModel>();
                ((FrameworkElement)d).DataContext = uViewModel;
            }
            else
            {
                var viewModel = ContainerHelper.Container.Resolve(viewModelType); 
                ((FrameworkElement)d).DataContext = viewModel;
            }           
        }
    }
}

我已经看到了很多关于列表框项目和鼠标事件等的其他线程。以至于我对要走哪条路线感到困惑。

Various options

ItemContainerStyles&Commands

我想在后面的代码中有一些东西并不是那么糟糕,对于像我这样忘记了一点我知道的 WPF 和 MVVM 的人来说,这看起来相当容易,但是因为我已经有了 RelayCommand、BindableBase 和 ViewModelLocator,感觉好像将鼠标事件(双击和右键单击)与命令连接起来会更好,但我不太确定如何。 所以,假设我在 TAEngineViewModel 中有一个 OpenFile 方法,它应该打开底层文件,其名称显示在 VS 编辑器的 ListBox 项目中(如果它被双击),我应该在 XAML 中放什么? 如何将选定的 CalcFile/ParameterFile 对象传递给 TAEngineViewModel?

我假设右键事件会和双击类似,如果不是,会有什么不同?

【问题讨论】:

标签: c# wpf xaml mvvm listbox


【解决方案1】:

据我所知,您需要一种机制来获取列表项单击/双击事件并将其重定向到视图模型中的特定方法。这是我的建议:

  1. 通过 nuget 找到一个名为 Caliburn.Micro 的现代且舒适的 MVVM 框架,并将其安装到您的项目中(您可以使用 nuget 控制台安装包,这里是解释链接https://www.nuget.org/packages/Caliburn.Micro),。
  2. 如果您没有连接到 TAEngineView 视图的视图模型,您应该创建它。此视图模型必须称为 TAEngineViewModel,因为您的视图模型定位器基于命名约定。
  3. 在视图模型中创建名为 OnItemClick 和 OnItemDoubleClick 的方法。
  4. 使用特定的 caliburn 语法将点击事件重定向到视图模型中的特定方法,这里是更多命令的链接https://caliburnmicro.codeplex.com/wikipage?title=Cheat%20Sheet&referringTitle=Documentation

这是一个视图模型代码:

public class TAEngineViewModel:IOnClickSupport
{
    private readonly ObservableCollection<ItemWithName> _parameterList;
    public ObservableCollection<ItemWithName> CalcFilesList => _parameterList;
    public ObservableCollection<ItemWithName> ParameterFilesList => _parameterList;

    public TAEngineViewModel()
    {
        _parameterList = new ObservableCollection<ItemWithName>
        {
            new ItemWithName(this) {Name = "AAAAAA"},
            new ItemWithName(this) {Name = "BBBBBB"},
            new ItemWithName(this) {Name = "CCCCCC"},
            new ItemWithName(this) {Name = "DDDDDD"}
        };
    }

    public void OnClick(object args)
    {

    }

    public void OnDoubleClick(object args )
    {

    }
}

public interface IOnClickSupport
{
    void OnClick(object args);
    void OnDoubleClick(object args);
}

public class ItemWithName:BaseObservableObject
{
    private readonly IOnClickSupport _support;
    private string _name;

    public ItemWithName(IOnClickSupport support)
    {
        _support = support;
    }

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    public void OnClick(object args)
    {
        _support.OnClick(args);
    }

    public void OnDoubleClick(object args)
    {
        _support.OnDoubleClick(args);
    }
}

查看代码如下:

<UserControl x:Class="SoCaliburnInvolvedWithEventsToCommand.TAEngineView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:cal="http://www.caliburnproject.org"
         xmlns:soCaliburnInvolvedWithEventsToCommand="clr-namespace:SoCaliburnInvolvedWithEventsToCommand"
         soCaliburnInvolvedWithEventsToCommand:ViewModelLocator.AutoWireViewModel="True">
<UserControl.Resources>
    <DataTemplate x:Key="DataTemplateWithTextBlockInside">
        <TextBlock Text="{Binding Path=Name}" />
    </DataTemplate>
    <DataTemplate x:Key="ItemTemplate">
        <StackPanel Orientation="Horizontal">
            <ContentControl cal:Message.Attach="[Event MouseDoubleClick] = [Action OnDoubleClick($eventArgs)];[Event MouseRightButtonDown] = [Action OnClick($eventArgs)]"
                            Content="{Binding}"
                            ContentTemplate="{StaticResource DataTemplateWithTextBlockInside}" />
        </StackPanel>
    </DataTemplate>
</UserControl.Resources>
<Grid>
    <TabControl x:Name="tabControl"
                    Width="317"
                    Height="490"
                    Margin="19,44,-36,-234"
                    HorizontalAlignment="Left"
                    VerticalAlignment="Top">
        <TabItem Header="Parameter Files">
            <ListBox Margin="20"
                         ItemTemplate="{StaticResource ItemTemplate}"
                         ItemsSource="{Binding ParameterFilesList}" />
        </TabItem>
        <TabItem Header="Calc Files">
            <ListBox Margin="20"
                         ItemTemplate="{StaticResource ItemTemplate}"
                         ItemsSource="{Binding CalcFilesList}" />
        </TabItem>
    </TabControl>
    <Label x:Name="label"
               Width="367            "
               Margin="19,13,0,0"
               HorizontalAlignment="Left"
               VerticalAlignment="Top"
               BorderThickness="2"
               Content="{Binding Path=Title}" />

</Grid></UserControl>

上述解决方案的缺点是底层存在循环关系(视图模型被传递到每个子项以支持具有主要 OnClick/OnDoubleClick 逻辑的子项),您可以通过使用一种事件聚合来避免这种关系。我可以建议你使用 RX 扩展。它是连接两个对象的一种灵活方式。为了开始使用 RX,你应该在你的 VS2015 中instal it via Nuget

这是一个例子:

public class TAEngineViewModel:IDisposable
{
    private IList<IDisposable> _disposablesChildrenList = new List<IDisposable>();
    private readonly ObservableCollection<ItemWithName> _parameterList;
    public ObservableCollection<ItemWithName> CalcFilesList => _parameterList;
    public ObservableCollection<ItemWithName> ParameterFilesList => _parameterList;

    public TAEngineViewModel()
    {
        _parameterList = new ObservableCollection<ItemWithName>
        {
            new ItemWithName {Name = "AAAAAA"},
            new ItemWithName {Name = "BBBBBB"},
            new ItemWithName {Name = "CCCCCC"},
            new ItemWithName {Name = "DDDDDD"}
        };
        Subscribe(_parameterList);

    }

    private  void Subscribe(ObservableCollection<ItemWithName> parameterList)
    {
        foreach (var itemWithName in parameterList)
        {
            var onRightClickObservableSubscription = itemWithName.OnRightClickObservable.Subscribe(OnClick);
            var onDoubleClickObservableSubscription = itemWithName.OnDoubleClickObservable.Subscribe(OnDoubleClick);
            _disposablesChildrenList.Add(onDoubleClickObservableSubscription);
            _disposablesChildrenList.Add(onRightClickObservableSubscription);
        }
    }

    public void OnClick(IItemArguments args)
    {
        Debug.WriteLine($"{args.SpecificItemWithName.Name} evet {args.SpecificEventArgs.GetType().Name}");
    }

    public void OnDoubleClick(IItemArguments args )
    {
        Debug.WriteLine($"{args.SpecificItemWithName.Name} evet {args.SpecificEventArgs.GetType().Name}");
    }

    public void Dispose()
    {
        foreach (var disposable in _disposablesChildrenList)
        {
            disposable.Dispose();
        }
    }
}

public interface IItemArguments
{
    ItemWithName SpecificItemWithName { get;}
    object SpecificEventArgs { get;}
}

public class ItemArguments : IItemArguments
{
    public ItemArguments(ItemWithName item, object args)
    {
        SpecificItemWithName = item;
        SpecificEventArgs = args;
    }

    public ItemWithName SpecificItemWithName { get; }

    public object SpecificEventArgs { get; }
}

public class ItemWithName:BaseObservableObject
{
    private string _name;

    private Subject<IItemArguments> _onDoubleClick = new Subject<IItemArguments>();
    private Subject<IItemArguments> _onClick = new Subject<IItemArguments>();
    public IObservable<IItemArguments> OnDoubleClickObservable;
    public IObservable<IItemArguments> OnRightClickObservable;

    public ItemWithName()
    {
        OnDoubleClickObservable = _onDoubleClick.AsObservable();
        OnRightClickObservable = _onClick.AsObservable();
    }

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            OnPropertyChanged();
        }
    }

    public void OnClick(object args)
    {
        _onClick.OnNext(new ItemArguments(this, args));
    }

    public void OnDoubleClick(object args)
    {
        _onDoubleClick.OnNext(new ItemArguments(this, args));
    }
}

如您所见,每次调用 OnClick/OnDoubleClick 方法时,都会将特定(选定)项传递给视图模型。 就这些。 如果您需要更多解释,请告诉我。 最好的问候。

【讨论】:

  • 谢谢,暂时我只是使用代码隐藏,但我以后可能会回到这个答案。
猜你喜欢
  • 2011-10-04
  • 1970-01-01
  • 1970-01-01
  • 2016-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多