Catel框架下CommandManager注册命令未触发问题:TabControl子ViewModel无法响应外部按钮命令
我有一个包含TabControl的View,每个标签页对应不同的ViewModel。标签页上方有一个Save按钮,用于保存当前激活标签页的内容。我尝试使用Catel的CommandManager将按钮的点击事件传递给对应的ViewModel,但命令并未触发。以下是我的实现代码:
MainWindow代码
using Catel.MVVM; using Catel.Windows; namespace Views public partial class MainWindow : DataWindow { private readonly CommandManagerWrapper _commandManagerWrapper; public MainWindow() : base(DataWindowMode.Custom, null, DataWindowDefaultButton.None, true, InfoBarMessageControlGenerationMode.None) { InitializeComponent(); _commandManagerWrapper = new CommandManagerWrapper(this); } }
CustomerOrderBlocksWindow视图代码
<catel:DataWindow x:Class="Views.CustomerOrderBlocksWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:catel="http://catel.codeplex.com" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:b="clr-namespace:TrendzzForYou.Helpers" xmlns:controls="clr-namespace:Views" xmlns:helpers="clr-namespace:Helpers" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" ResizeMode="CanResize" SizeToContent="WidthAndHeight" WindowStartupLocation="CenterScreen" WindowState="Normal" Icon="/Resources/Graphics/Filters64.png" MinHeight="1000" MinWidth="1800"> <catel:DataWindow.Resources> <DataTemplate x:Key="TotalOrderTemplate"> <controls:CustomerOrderTotalControl DataContext="{Binding CurrentTabContentViewModel}"/> </DataTemplate> <DataTemplate x:Key="NonDeliverableItemsTemplate"> <controls:CustomerOrderNonDeliverableControl DataContext="{Binding CurrentTabContentViewModel}"/> </DataTemplate> <helpers:CustomerOrderBlocksTemplateSelector x:Key="selector" TotalOrderTemplate="{StaticResource TotalOrderTemplate}" NonDeliverableItemsTemplate="{StaticResource NonDeliverableItemsTemplate}"/> </catel:DataWindow.Resources> <Grid Margin="0,0,10,0"> <Grid.RowDefinitions> <RowDefinition Height="170"/> <RowDefinition MinHeight="750"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition MinWidth="1800"/> </Grid.ColumnDefinitions> <StackPanel> <telerik:RadButton Command="{catel:CommandManagerBinding Accept}" ToolTip="Sla gegevens op en voer volgende order in" TabIndex="99" Grid.Row="0"> <StackPanel Orientation="Horizontal"> <Image Source="/Resources/Graphics/Accept16.png" Height="16" /> <telerik:Label Content="Opslaan" /> </StackPanel> </telerik:RadButton> </StackPanel> <telerik:RadTabControl Grid.Row="1" Grid.Column="0" DisplayMemberPath="Header" ContentTemplateSelector="{StaticResource selector}" ItemsSource="{Binding Tabs}" SelectedIndex="{Binding CustomerOrderTab}"/> </catel:DataWindow>
CustomerOrderBlocksWindowViewModel代码
using Catel; using Catel.Data; using Catel.IoC; using Catel.MVVM; using Catel.Services; using System.Collections.Generic; public class CustomerOrderBlocksWindowViewModel : ViewModelBase { public static readonly PropertyData CustomerOrderTabProperty = RegisterProperty("CustomerOrderTab", typeof(int)); public static readonly PropertyData TabsProperty = RegisterProperty("Tabs", typeof(List<TabItemModel>)); public int CustomerOrderTab { get { return GetValue<int>(CustomerOrderTabProperty); } set { SetValue(CustomerOrderTabProperty, value); } } public List<TabItemModel> Tabs { get { return GetValue<List<TabItemModel>>(TabsProperty); } set { SetValue(TabsProperty, value); } } public CustomerOrderBlocksWindowViewModel(IUIVisualizerService theVisualizerService, IMessageService theMessageService, IPleaseWaitService thePleaseWaitService) { Argument.IsNotNull(() => theVisualizerService); Argument.IsNotNull(() => theMessageService); Argument.IsNotNull(() => thePleaseWaitService); uiVisualizer = theVisualizerService; messageService = theMessageService; pleaseWait = thePleaseWaitService; var dependencyResolver = IoCConfiguration.DefaultDependencyResolver; commandManager = dependencyResolver.Resolve<ICommandManager>(); commandManager.CreateCommand("Accept"); UpdateTabs(); } private void UpdateTabs() { Tabs = new List<TabItemModel>() { new TabItemModel() { Header="Totaal" } }; switch(CustomerOrderTab) { case 0: Tabs[0].CurrentTabContentViewModel = new CustomerOrderTotalControlViewModel(uiVisualizer, messageService, pleaseWait, commandManager); break; default: break; } } }
CustomerOrderTotalControlViewModel代码
using Catel; using Catel.Data; using Catel.MVVM; using Catel.Services; namespace ViewModels { public class CustomerOrderTotalControlViewModel : ViewModelBase { public Command CmdAccept { get; private set; } #region private references private IMessageService messageService; private IPleaseWaitService pleaseWait; private IUIVisualizerService uiVisualizer; #endregion public CustomerOrderTotalControlViewModel(IUIVisualizerService theVisualizerService, IMessageService theMessageService, IPleaseWaitService thePleaseWaitService, ICommandManager commandManager) { Argument.IsNotNull(() => theVisualizerService); Argument.IsNotNull(() => theMessageService); Argument.IsNotNull(() => thePleaseWaitService); Argument.IsNotNull(() => commandManager); uiVisualizer = theVisualizerService; messageService = theMessageService; pleaseWait = thePleaseWaitService; CmdAccept = new Command(OnAccept); commandManager.RegisterCommand("Accept", CmdAccept, this); } private void OnAccept() { WaitScreen("Order opslaan..."); } private void WaitScreen(string TheMessage) { if (TheMessage != null) pleaseWait.Show(TheMessage); else pleaseWait.Hide(); } } }
TabItemModel代码
using Catel.MVVM; namespace Models { public class TabItemModel { /// <summary> /// Tab Header text /// </summary> public string Header { get; set; } /// <summary> /// Tab content /// </summary> public IViewModel CurrentTabContentViewModel { get; set; } } }
根据Catel文档,我期望点击按钮时CustomerOrderTotalControlViewModel中的CmdAccept命令能被触发,但实际并未生效。请问我遗漏了什么或者哪里操作有误?
我帮你梳理下几个关键问题点,这些应该就是导致命令没触发的原因:
1. CommandManagerBinding的目标上下文错误
你的按钮在父View(CustomerOrderBlocksWindow)中,{catel:CommandManagerBinding Accept}默认会查找**当前View关联的ViewModel(也就是CustomerOrderBlocksWindowViewModel)**中的命令,而不是子ViewModel的命令。你需要明确告诉CommandManager要把命令转发到当前激活的子ViewModel。
修正步骤:
- 在父ViewModel中添加当前激活Tab的ViewModel属性:
public static readonly PropertyData CurrentActiveTabViewModelProperty = RegisterProperty("CurrentActiveTabViewModel", typeof(IViewModel)); public IViewModel CurrentActiveTabViewModel { get { return GetValue<IViewModel>(CurrentActiveTabViewModelProperty); } set { SetValue(CurrentActiveTabViewModelProperty, value); } } public static readonly PropertyData CurrentActiveTabItemProperty = RegisterProperty("CurrentActiveTabItem", typeof(TabItemModel)); public TabItemModel CurrentActiveTabItem { get { return GetValue<TabItemModel>(CurrentActiveTabItemProperty); } set { SetValue(CurrentActiveTabItemProperty, value); // 同步当前激活的子ViewModel if (value != null) { CurrentActiveTabViewModel = value.CurrentTabContentViewModel; } } }
- 修改TabControl的XAML绑定,关联选中项:
<telerik:RadTabControl Grid.Row="1" Grid.Column="0" DisplayMemberPath="Header" ContentTemplateSelector="{StaticResource selector}" ItemsSource="{Binding Tabs}" SelectedItem="{Binding CurrentActiveTabItem}" SelectedIndex="{Binding CustomerOrderTab}"/>
- 最后修改按钮的Command绑定,指定目标为当前激活的子ViewModel:
<telerik:RadButton Command="{catel:CommandManagerBinding Accept, Target={Binding CurrentActiveTabViewModel}}" ToolTip="Sla gegevens op en voer volgende order in" TabIndex="99" Grid.Row="0">
2. 子ViewModel的生命周期管理问题
你手动实例化子ViewModel的方式(new CustomerOrderTotalControlViewModel(...))绕过了Catel的ViewModel定位器和生命周期管理,可能导致子ViewModel没有被Catel的视图模型管理器正确跟踪,进而影响CommandManager的命令关联。
修正建议:
如果不是必须手动创建,建议让Catel通过ViewModelLocator自动创建子ViewModel,把DataTemplate里的DataContext绑定改成{catel:ViewModel}:
<DataTemplate x:Key="TotalOrderTemplate"> <controls:CustomerOrderTotalControl DataContext="{catel:ViewModel ViewModelType=ViewModels.CustomerOrderTotalControlViewModel}"/> </DataTemplate>
如果必须手动创建,记得调用ViewModelManager.RegisterViewModel注册子ViewModel,让Catel感知到它的存在:
var vm = new CustomerOrderTotalControlViewModel(uiVisualizer, messageService, pleaseWait, commandManager); ViewModelManager.Default.RegisterViewModel(vm); Tabs[0].CurrentTabContentViewModel = vm;
3. 子窗口的CommandManagerWrapper缺失
你的MainWindow里创建了CommandManagerWrapper,但CustomerOrderBlocksWindow作为独立的DataWindow,也需要创建自己的CommandManagerWrapper,确保命令绑定能关联到当前窗口的上下文:
public partial class CustomerOrderBlocksWindow : DataWindow { private readonly CommandManagerWrapper _commandManagerWrapper; public CustomerOrderBlocksWindow() : base(DataWindowMode.Custom, null, DataWindowDefaultButton.None, true, InfoBarMessageControlGenerationMode.None) { InitializeComponent(); _commandManagerWrapper = new CommandManagerWrapper(this); } }
4. 命令的CanExecute逻辑补充
虽然默认Command的CanExecute是true,但显式指定可以避免潜在的触发问题:
CmdAccept = new Command(OnAccept, () => true);
按照上面的步骤调整后,应该就能让按钮正确触发当前激活Tab对应的子ViewModel的Accept命令了。
内容的提问来源于stack exchange,提问作者JeroenZenM

