WPF - 列表框项目上的两个命令
我正在开发一个用于VS插件中的功能的VS程序包扩展。WPF - 列表框项目上的两个命令
插件会将文件加载到工具栏窗口,然后如果用户双击一个项目(这是一个文件的名称),该文件将在VS中的编辑器中打开。如果用户右键单击在一个项目上,它会弹出一个菜单。所以我的问题是关于如何将列表框项目的这些操作(双击和右键单击)与我现有的代码连接起来的最佳方式。
对于扩展我们使用WPF,但对于插件它是Windows窗体。 不过,我对WPF不是很熟悉。大约一年前,我观看了Brian Noyes的Pluralsight课程“WPF MVVM深度”,并在扩展中实现了一些内容,但是在今年的大部分时间里,我还没有开发过扩展。其结果是,我只能模糊地回忆起我编写的代码,而对于最佳设计是什么,我有点困惑。
那么让我告诉你,我已经有了:
这里是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<CalcFile>
类型,ObservableCollection<Parameter>
型ParameterFilesList。
然后,我已经有这个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;
}
}
}
}
我见过不少关于Listbox项目和鼠标事件的其他线程等等,以至于让我感到困惑要走哪条路线。
我猜有东西在后面的代码并没有那么糟糕,它看起来相当容易对像我这样谁已经忘记我知道WPF和MVVM的点点,但因为我已经有了RelayCommand,BindableBase和ViewModelLocator,所以感觉好像它将更好地将鼠标事件(双击和右键单击)与命令连接起来,但我不太确定如何。 因此,假设我在TAEngineViewModel中有一个OpenFile方法,它应该在VS Editor中打开名称显示在ListBox项目中的底层文件(如果它是双击的话),那么我应该在XAML中放置什么? 如何将所选的CalcFile/ParameterFile对象传递给TAEngineViewModel?
我假设右键单击事件将类似于双击,如果不是,它将如何不同?
据我所知,你需要一个机制,将采取列表项点击/ doubleclick事件,并将其重定向到您的视图模型中的具体方法。这里是我的建议:
- 找到名为Caliburn.Micro一个现代而舒适的MVVM框架通过的NuGet并将其安装到您的项目(可以使用的NuGet控制台安装包,在这里的解释是链接https://www.nuget.org/packages/Caliburn.Micro) 。
- 如果你没有被连接到您的TAEngineView查看您应该创建一个视图模型。这个视图模型必须被称为TAEngineViewModel,因为你的视图模型定位器是基于命名约定的。
- 您的视图模型内部创建方法调用了OnItemClick和OnItemDoubleClick。
- 使用特定卡利语法的单击事件的具体方法这里重定向您的视图模型里面是有多个命令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被调用时,传递给视图模型的特定的(选定的)项目。 就是这样。 让我知道你是否需要更多解释。 此致敬礼。
谢谢,暂时我我只是简单地使用代码隐藏,但我可能会在未来回到这个答案。 – Igavshne
你可以看看这里http://stackoverflow.com/questions/11172443/how-to-fire-a-command-on-double-click-listbox-item-using-mvvm –