Cinch V2:我的 Cinch MVVM 框架的第 2 版:第 3 部分






4.96/5 (42投票s)
如果 Jack Daniels 制造 MVVM 框架。
目录
引言
上次我们讨论了 Cinch V2 服务。在本文中,我们将研究 Cinch V2 的全新内容,并在适当的情况下,我将向您展示它是否正在替换一些 Cinch V1 功能。
正如我所承诺的,在每篇文章中,我都会展示 Cinch V2 兼容性矩阵。
兼容性矩阵列出了类及其一般工作区域,以及它们是否与WPF或SL或两者兼容。
工作区域 | 类名 | WPF | Silverlight(4或更高版本) | 两者 |
业务对象 | EditableValidatingObject.cs | 是 | ||
业务对象 | ValidatingObject.cs | 是 | ||
业务对象 | DataWrapper.cs | 是 | ||
Commands | EventToCommandArgs.cs | 是 | ||
Commands | SimpleCommand.cs | 是 | ||
Commands | WeakEventHandlerManager.cs | 是 | ||
事件 | CloseRequestEventArgs.cs | 是 | ||
事件 | UICompletedEventArgs.cs | 是 | ||
弱事件 | WeakEvent.cs | 是 | ||
弱事件 | WeakEventHelper.cs | 是 | ||
弱事件 | WeakEventProxy.cs | 是 | ||
扩展方法 | DispatcherExtensions.cs | 是 | ||
扩展方法 | GenericListExtensions.cs | 是 | ||
交互行为 | CommandDrivenGoToStateAction.cs | 是 | ||
交互行为 | FocusBehaviourBase.cs | 是 | ||
交互行为 | NumericTextBoxBehaviour.cs | 是 | ||
交互行为 | SelectorDoubleClickCommandBehavior.cs | 是 | ||
交互行为 | TextBoxFocusBehavior.cs | 是 | ||
交互触发器 | CompletedAwareCommandTrigger.cs | 是 | ||
交互触发器 | CompletedAwareGotoStateCommandTrigger.cs | 是 | ||
交互触发器 | EventToCommandTrigger.cs | 是 | ||
消息中介者 | MediatorMessageSinkAttribute.cs | 是 | ||
消息中介者 | MediatorSingleton.cs | 是 | ||
服务实现 | ChildWindowService.cs | 是 | ||
服务实现 | SLMessageBoxService.cs | 是 | ||
服务实现 | ViewAwareStatus.cs | 是 | ||
服务实现 | ViewAwareStatusWindow.cs | 是 | ||
服务实现 | VSMService.cs | 是 | ||
服务实现 | WPFMessageBoxService.cs | 是 | ||
服务实现 | WPFOpenFileService.cs | 是 | ||
服务实现 | WPFSaveFileService.cs | 是 | ||
服务实现 | WPFUIVisualizerService.cs | 是 | ||
服务接口 | IChildWindowService.cs | 是 | ||
服务接口 | IMessageBoxService.cs | 是 | ||
服务接口 | IViewAwareStatus.cs | 是 | ||
服务接口 | IViewAwareStatusWindow.cs | 是 | ||
服务接口 | IVSM.cs | 是 | ||
服务接口 | IMessageBoxService.cs | 是 | ||
服务接口 | IOpenFileService.cs | 是 | ||
服务接口 | ISaveFileService.cs | 是 | ||
服务接口 | IUIVisualizerService.cs | 是 | ||
服务测试实现 | TestChildWindowService.cs | 是 | ||
服务测试实现 | TestMessageBoxService.cs | 是 | ||
服务测试实现 | TestViewAwareStatus.cs | 是 | ||
服务测试实现 | TestViewAwareStatusWindow.cs | 是 | ||
服务测试实现 | TestVSMService.cs | 是 | ||
服务测试实现 | TestMessageBoxService.cs | 是 | ||
服务测试实现 | TestOpenFileService.cs | 是 | ||
服务测试实现 | TestSaveFileService.cs | 是 | ||
服务测试实现 | TestUIVisualizerService.cs | 是 | ||
多线程 | AddRangeObservableCollection.cs(这是一个特定的 Silverlight 实现) | 是 | ||
多线程 | AddRangeObservableCollection.cs(这是特定的 WPF 实现) | 是 | ||
多线程 | BackgroundTaskManager.cs | 是 | ||
多线程 | ISynchronizationContext.cs | 是 | ||
多线程 | UISynchronizationContext.cs | 是 | ||
多线程 | ApplicationHelper.cs | 是 | ||
多线程 | DispatcherNotifiedObservableCollection.cs | 是 | ||
菜单 | CinchMenuItem.cs | 是 | ||
实用程序 | ArgumentValidator.cs | 是 | ||
实用程序 | IWeakEventListener.cs(这是 Silverlight 缺少的一个 System 类,所以我创建了它) |
是 | ||
实用程序 | ObservableHelper.cs | 是 | ||
实用程序 | PropertyChangedEventManager.cs(这是 Silverlight 缺少的一个 System 类,所以我创建了它) |
是 | ||
实用程序 | PropertyObserver.cs | 是 | ||
实用程序 | BindingEvaluator.cs | 是 | ||
实用程序 | ObservableDictionary.cs | 是 | ||
实用程序 | TreeHelper.cs | 是 | ||
验证 | RegexRule.cs | 是 | ||
验证 | Rule.cs | 是 | ||
验证 | SimpleRule.cs | 是 | ||
ViewModels | EditableValidatingViewModelBase.cs | 是 | ||
ViewModels | IViewStatusAwareInjectionAware.cs | 是 | ||
ViewModels | ValidatingViewModelBase.cs | 是 | ||
ViewModels | ViewMode.cs | 是 | ||
ViewModels | ViewModelBase.cs | 是 | ||
ViewModels | ViewModelBaseSLSpecific.cs | 是 | ||
ViewModels | ViewModelBaseWPFSpecific.cs | 是 | ||
Workspaces | ChildWindowResolver.cs | 是 | ||
Workspaces | CinchBootStrapper.cs(Silverlight 版本) | 是 | ||
Workspaces | CinchBootStrapper.cs(WPF版本) | 是 | ||
Workspaces | PopupNameToViewLookupKeyMetadataAttribute.cs | 是 | ||
Workspaces | IWorkspaceAware.cs | 是 | ||
Workspaces | MockView.cs | 是 | ||
Workspaces | NavProps.cs | 是 | ||
Workspaces | PopupResolver.cs | 是 | ||
Workspaces | ViewnameToViewLookupKeyMetadataAttribute.cs | 是 | ||
Workspaces | ViewResolver.cs | 是 | ||
Workspaces | WorkspaceData.cs | 是 |
现在我已经向您展示了哪些类将与 WPF/Silverlight 一起使用,我们继续本文的其余部分,好吗?但首先,这里是旧 Cinch V1 文章的链接。
如果您错过了 Cinch V1,并且对 MVVM 感兴趣,我强烈建议您首先阅读所有 Cinch V1 文章,因为这将使您对这些 Cinch V2 文章中将要呈现的内容有更深入的理解。
Cinch V1 文章链接
有些人可能从未见过旧的 Cinch V1 文章,因此我也会在此处列出它们,因为 Cinch V2 仍然使用与 Cinch V1 相同的功能,我将把您重定向到这些文章。
- Cinch入门文章
- Cinch及其内部机制的演练 I
- Cinch及其内部机制的演练 II
- 如何使用Cinch开发ViewModels
- 如何使用 Cinch 进行 ViewModel 单元测试,包括如何测试可能在 Cinch ViewModel 中运行的 BackgroundWorker 线程
- 使用Cinch的演示应用程序
Cinch V2 文章链接
- Cinch V2:介绍、MEFedMVVM 和 ViewModel/服务解析
- Cinch V2:服务/UI 服务
- Cinch V2:全新功能(本文)
- Cinch V2:深入探讨已更改/未更改的内容
- Cinch V2:剖析 WPF 演示应用程序
- Cinch V2:剖析 Silverlight 4 演示应用程序
好的,这就是文章路线图的样子,所以我想现在是时候深入研究本文的实质内容了,让我们开始吧
Cinch V2 的新功能
现在我们可以深入研究本文的实质内容,这实际上是 Cinch V2 中添加的新内容;其中一些是对 Cinch V1 内容的重写,已根据当前最佳实践(例如使用 Blend 交互性)进行了重写。
SimpleCommand
这是 Cinch V1 中提供的 SimpleCommand
的重写。
那么,有什么变化呢?嗯,实际上变化很大
- 我向它添加了两个构造函数,使其声明新的
SimpleCommand
变得微不足道。 - 我添加了一个弱
CommandCompleted
事件(这非常有用,您稍后会看到更多相关信息)。 - 我向它添加了两个泛型参数,这样
ICommand.CanExecute
参数和ICommand.Execute
参数可以声明为具有不同的参数类型。
那么新的 SimpleCommand<T1,T2>
是什么样子的呢?
using System.Windows.Input;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Cinch
{
/// <summary>
/// Interface that is used for ICommands that notify when they are
/// completed
/// </summary>
public interface ICompletionAwareCommand
{
/// <summary>
/// Notifies that the command has completed
/// </summary>
WeakActionEvent<object> CommandCompleted { get; set; }
}
/// <summary>
/// Simple delegating command, based largely on DelegateCommand from PRISM/CAL
/// </summary>
/// <typeparam name="T1">The type for the ICommand.CanExecute() parameter</typeparam>
/// <typeparam name="T2">The type for the ICommand.Execute() parameter</typeparam>
public class SimpleCommand<T1,T2> : ICommand, ICompletionAwareCommand
{
private Func<T1, bool> canExecuteMethod;
private Action<T2> executeMethod;
private WeakActionEvent<object> commandCompleted;
public SimpleCommand(Func<T1, bool> canExecuteMethod, Action<T2> executeMethod)
{
this.executeMethod = executeMethod;
this.canExecuteMethod = canExecuteMethod;
this.CommandCompleted = new WeakActionEvent<object>();
}
public SimpleCommand(Action<T2> executeMethod)
{
this.executeMethod = executeMethod;
this.canExecuteMethod = (x) => { return true; };
this.CommandCompleted = new WeakActionEvent<object>();
}
public WeakActionEvent<object> CommandCompleted { get; set;}
public bool CanExecute(T1 parameter)
{
if (canExecuteMethod == null) return true;
return canExecuteMethod(parameter);
}
public void Execute(T2 parameter)
{
if (executeMethod != null)
{
executeMethod(parameter);
}
//now raise CommandCompleted for this ICommand
WeakActionEvent<object> completedHandler = CommandCompleted;
if (completedHandler != null)
{
completedHandler.Invoke(parameter);
}
}
public bool CanExecute(object parameter)
{
return CanExecute((T1)parameter);
}
public void Execute(object parameter)
{
Execute((T2)parameter);
}
#if SILVERLIGHT
/// <summary>
/// Occurs when changes occur that affect whether the command should execute.
/// </summary>
public event EventHandler CanExecuteChanged;
#else
/// <summary>
/// Occurs when changes occur that affect whether the command should execute.
/// </summary>
public event EventHandler CanExecuteChanged
{
add
{
if (canExecuteMethod != null)
{
CommandManager.RequerySuggested += value;
}
}
remove
{
if (canExecuteMethod != null)
{
CommandManager.RequerySuggested -= value;
}
}
}
#endif
/// <summary>
/// Raises the <see cref="CanExecuteChanged" /> event.
/// </summary>
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
Justification = "The this keyword is used in the Silverlight version")]
[SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate",
Justification = "This cannot be an event")]
public void RaiseCanExecuteChanged()
{
#if SILVERLIGHT
var handler = CanExecuteChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
#else
CommandManager.InvalidateRequerySuggested();
#endif
}
}
}
一些眼尖的人可能已经注意到 CommandCompleted WeakActionEvent
,我们将在下一节讨论。
那么我们现在如何使用这些新的改进的 SimpleCommand<T1,T2>
呢?嗯,现在实际上非常简单,您只需要这样做
第 1 步:为命令添加属性
您可以通过以下方式从 ViewModel 公开 SimpleCommand<T1,T2>
属性
public SimpleCommand<Object, Object> OpenExistingFileCommand { get; private set; }
第 2 步:构造新命令
以下是您如何构造实际命令(此示例假定命令始终可以执行,因此未提供 CanExecute
委托)
OpenExistingFileCommand = new SimpleCommand<Object, Object>(ExecuteOpenExistingFileCommand);
第 3 步:添加命令方法
以下是 Execute
方法的示例
private void ExecuteOpenExistingFileCommand(Object args)
{
.....
.....
}
操作/触发器
现在我是一个附加 DP 的狂热粉丝,但当更好的东西出现时,我也愿意屈服。不久前,Blend Interactity.dll 带来了更好的东西,它包含 Actions/Behaviours/Triggers 的基类,所有这些都或多或少地源于很多人已经在用 Attached DPs 做的事情。基本上,Blend Interactity.dll 包含的基类模仿了 WPF/Silverlight 社区已经在用 Attached DPs 做的事情,只是将其形式化为一种模式,允许 Blend 用户简单地将这些类拖到设计界面上并更改几个属性,然后……奇迹发生了。
无论如何,话虽如此,我以前在 Cinch V1 中有一堆 Attached DPs,并且总的来说,这些只是演变成了 Blend Interactivity Actions/Behaviours/Triggers,但这里也有一些新的。
您可以使用标准 Blend 资产选项卡访问这些新的 Cinch V2 操作/行为/触发器
那么,让我们继续看看 Cinch V2 为我们提供了什么。
行为
Cinch V2 提供了以下行为
NumericTextBoxBehaviour(仅 WPF,Cinch V1 中也作为 Attached DP 提供)
这是一个非常标准的行为,它只是让 TextBox
只接受数字数据。
以下是您在 XAML 中使用它的方法
<TextBox Text="{Binding ImageRating.DataValue, UpdateSourceTrigger=LostFocus,
ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
Style="{StaticResource ValidatingTextBox}"
IsEnabled="{Binding ImageRating.IsEditable}">
<i:Interaction.Behaviors>
<CinchV2:NumericTextBoxBehaviour/>
</i:Interaction.Behaviors>
</TextBox>
SelectorDoubleClickCommandBehavior(仅 WPF,Cinch V1 中也作为 Attached DP 提供)
这S也是一个非常标准的行为,可以用来在 Selector
项被双击时触发 ViewModel 命令;尽管 WPF 演示应用程序没有这方面的示例,但您可以在 XAML 中这样使用它
<ListView ItemsSource="{Binding People}" IsSynchronizedWithCurrentItem="True">
<i:Interaction.Behaviors>
<CinchV2:SelectorDoubleClickCommandBehavior Command="{Binding SomeViewModelCommand}" />
</i:Interaction.Behaviors>
</TextBox>
如果您不关心 EventArgs
进入 ViewModel,您可以简单地像这样声明 ViewModel 代码
//declare command
public SimpleCommand<Object, Object> SelectorDoubleClickCommand { get; private set; }
//initalise command
SelectorDoubleClickCommand =
new SimpleCommand<Object, Object>(ExecuteSelectorDoubleClickCommand);
//command handler
private void ExecuteSelectorDoubleClickCommand(Object args)
{
//do something
}
但是,如果您想了解导致命令触发的 EventArgs
,您可以在 ViewModel 中执行以下操作
//Command property in ViewModel
public SimpleCommand<Object, EventToCommandArgs>
SelectorDoubleClickCommand { get; private set; }
//Where you setup your Command in your ViewModel
SelectorDoubleClickCommand =
new SimpleCommand<Object, EventToCommandArgs>(ExecuteSelectorDoubleClickCommand);
//Command handlers
private void ExecuteSelectorDoubleClickCommand(EventToCommandArgs args)
{
ICommand commandRan = args.CommandRan;
Object o = args.CommandParameter; //get command parameter
EventArgs ea = args.EventArgs; //get event args
var sender = args.Sender; //get orginal sender (ListView in this case)
}
可以看出,您可以获得所有相关信息,例如
- 命令参数
EventArgs
sender
(事件源,在此示例中为ListView
)
注意:我不主张在 ViewModel 中包含 UI 类型对象,因为它更难测试,但有些人喜欢它,所以我确实在 args.EventArgs
/ args.Sender
对象中提供了它,但我不得不说我从未在我编写的任何 ViewModel 代码中做过这种事情。因此,请自行承担风险使用它,当您无法正确测试 ViewModel 时,不要责怪我,我已经警告过您了。
TextBoxFocusBehavior(仅 WPF)
Cinch V2 还提供了一种让 ViewModel 在 View 中设置焦点的方法。基本思想是这样
ViewModelBase
类有一个公共方法RaiseFocusEvent(String focusProperty)
,它会引发一个名为FocusRequested
的ViewModelBase
事件- 然后有一个名为
TextBoxFocusBehavior
的 Blend 行为,它会监听ViewModelBase FocusRequested
事件 - 当
TextBoxFocusBehavior
看到ViewModelBase FocusRequested
事件触发时,它会查看行为目标FrameworkElements (TextBox)
绑定是否与请求的属性名称匹配,如果匹配,焦点就会移动到行为目标对象 (TextBox
)
那么代码是什么样子的呢?嗯,从 ViewModelBase
类开始,有这些代码
public event Action<String> FocusRequested;
/// <summary>
/// Raises the Focus Requested event
/// </summary>
/// <param name="focusProperty"></param>
public void RaiseFocusEvent(String focusProperty)
{
FocusRequested(focusProperty);
}
然后我们在 TextBoxFocusBehavior
中有这样的东西
public class TextBoxFocusBehavior : FocusBehaviorBase
{
#region Protected Methods
protected DependencyProperty GetSourceProperty()
{
//As this is a TextBox we use TextBox.TextProperty
return TextBox.TextProperty;
}
#endregion
#region Overrides
protected override void OnAttached()
{
if (!(AssociatedObject is TextBoxBase))
return;
base.OnAttached();
AssociatedObject.Loaded += AssociatedObject_Loaded;
}
void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
if (AssociatedObject.DataContext is ViewModelBase)
((ViewModelBase)AssociatedObject.DataContext).FocusRequested +=
TextBoxFocusBehavior_FocusRequested;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.Loaded -= AssociatedObject_Loaded;
if (AssociatedObject.DataContext is ViewModelBase)
((ViewModelBase)AssociatedObject.DataContext).FocusRequested -=
TextBoxFocusBehavior_FocusRequested;
}
#endregion
#region Private Methods
private void TextBoxFocusBehavior_FocusRequested(String propertyPath)
{
Binding binding = BindingOperations.GetBinding(
AssociatedObject, GetSourceProperty());
base.ConductFocusOnElement(binding,propertyPath, IsUsingDataWrappers);
}
#endregion
#region DPs
#region IsUsingDataWrappers
/// <summary>
/// IsUsingDataWrappers Dependency Property
/// </summary>
public static readonly DependencyProperty IsUsingDataWrappersProperty =
DependencyProperty.Register("IsUsingDataWrappers",
typeof(bool), typeof(TextBoxFocusBehavior),
new FrameworkPropertyMetadata((bool)false));
/// <summary>
/// Gets or sets the IsUsingDataWrappers property.
/// </summary>
public bool IsUsingDataWrappers
{
get { return (bool)GetValue(IsUsingDataWrappersProperty); }
set { SetValue(IsUsingDataWrappersProperty, value); }
}
#endregion
#endregion
}
您可以看到此 Blend 行为也支持 DataWrapper
,您只需指定您要应用此行为的 TextBox
是否绑定到 DataWrapper
。您可能还会注意到此类别将一些工作委托给基类,实际的焦点工作就发生在那里,所以我们也来看看它。
/// <summary>
/// Provides a focus behaviour base class that attempts
/// to focus elements by matching their bound property paths
/// with a input propertyPath string
/// </summary>
public abstract class FocusBehaviorBase : Behavior<FrameworkElement>
{
#region Protected Methods
/// <summary>
/// Attempts to force focus to the bound property with the same propertyPath
/// as the propertyPath input
/// </summary>
/// <param name="elementBinding">Binding to evaluate</param>
/// <param name="propertyPath">propertyPath to try and find finding for</param>
/// <param name="isUsingDataWrappers">shoul be true if the property is bound to a
/// <c>Cinch.DataWrapper</c></param>
protected virtual void ConductFocusOnElement(Binding elementBinding,
String propertyPath, bool isUsingDataWrappers)
{
if (elementBinding == null)
return;
if (isUsingDataWrappers)
{
if (!elementBinding.Path.Path.Contains(propertyPath))
return;
}
else
{
if (elementBinding.Path.Path != propertyPath)
return;
}
// Delay the call to allow the current batch
// of processing to finish before we shift focus.
AssociatedObject.Dispatcher.BeginInvoke((Action)delegate
{
if (!AssociatedObject.Focus())
{
DependencyObject fs = FocusManager.GetFocusScope(AssociatedObject);
FocusManager.SetFocusedElement(fs, AssociatedObject);
}
},
DispatcherPriority.Background);
}
#endregion
}
看看这如何处理 DataWrapper
。无论如何,这些是内部机制;我们如何使用它呢?实际上非常简单。
在您的 ViewModel 中,每当您想为 TextBox
设置焦点时,请执行以下操作
RaiseFocusEvent("ImageRating");
在您的 TextBox
的 XAML 中,可以这样写
<TextBox Text="{Binding ImageRating.DataValue, UpdateSourceTrigger=LostFocus,
ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
Style="{StaticResource ValidatingTextBox}"
IsEnabled="{Binding ImageRating.IsEditable}">
<i:Interaction.Behaviors>
<CinchV2:TextBoxFocusBehavior IsUsingDataWrappers="true" />
</i:Interaction.Behaviors>
</TextBox>
如果您的 TextBox
绑定到的属性不是 DataWrapper
属性,只需将 IsUsingDataWrapper="false"
。
触发器
Cinch V2 提供以下触发器。
CompletedAwareCommandTrigger
大多数 WPF/Silverlight 用户都会非常习惯于从他们的视图调用 ViewModel 中的命令。但偶尔,您需要的是相反的,您需要 ViewModel 告诉视图做一些事情。在 Cinch V2 中,我通过提供一个 SimpleCommand
来提供这种机制,该命令在其运行其 Execute
委托时会触发一个 CommandCompleted
事件。然后可以将此命令用作触发器,以在视图中运行一些 Blend 操作。
Cinch V2 更进一步,提供了一个 Blend 触发器,它期望绑定到一个具有 CommandCompleted
事件的 SimpleCommand
,并在 CommandCompleted
事件触发时运行触发器的操作。
以下是触发器的完整代码
public class CompletedAwareCommandTrigger : TriggerBase<FrameworkElement>
{
#region DPs
#region Command
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICompletionAwareCommand),
typeof(CompletedAwareCommandTrigger), null);
/// <summary>
/// Gets or sets the Command property.
/// </summary>
public ICompletionAwareCommand Command
{
get { return (ICompletionAwareCommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
#endregion
#endregion
#region Overrides
/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
/// <remarks>
/// Override this to hook up functionality to the AssociatedObject.
/// </remarks>
protected override void OnAttached()
{
base.OnAttached();
this.Command.CommandCompleted += Command_Completed;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.Command.CommandCompleted -= Command_Completed;
}
#endregion
#region Private Method
private void Command_Completed(object parameter)
{
// Invoke the actions
InvokeActions(parameter);
}
#endregion
}
我们可以在一些 XAML 中这样使用它
<CinchV2:CompletedAwareCommandTrigger
Command="{Binding ShowActionsCommandReversed}">
<ei:GoToStateAction StateName="ShowActionsState"/>
</CinchV2:CompletedAwareCommandTrigger>
此示例使用标准的 Blend GoToStateAction
,但它可以是您喜欢的任何操作,只要您希望它在 ViewModel 中的 SimpleCommand
完成时触发
这可能是一些 ViewModel 代码的样子
//public property for the View to bind to
public SimpleCommand<Object, Object> ShowActionsCommandReversed { get; private set; }
// initialise the Command, do nothing with its Execute delegate
// some reverse commands, that the VM fires,
// and the View uses as CompletedAwareCommandTriggers
//to carry out some actions. In this case GoToStateActions are used in the View
ShowActionsCommandReversed = new SimpleCommand<Object, Object>((input) => { });
//Fire the command from the ViewModel, so the Views
//trigger can carry out any Actions associated
//with this command completing
ShowActionsCommandReversed.Execute(null);
CompletedAwareGotoStateCommandTrigger
我不知道你们有多少人知道,但标准的 VisualStateManager
可以用来以编程方式进入特定的 VisualState
,它只会在包含您试图进入的 VisualState
的 VisualStateGroup
直接位于您的 VisualTree
的根元素下方时才起作用。
事实上,这是一个合理的假设,因为这就是标准的 Blend GoToStateAction
期望的工作方式;如果您查看 VisulStateManager.GoToState
,您会看到它只接受一个控件,如下所示
但有时您可能需要将 VisualStateGroup
不直接放在 VisualTree
中的根节点下(好的,这很少见,但确实会发生),并且您可能希望它们所在的 FrameworkElement
可能不是控件。
现在我想很多人不知道,还有一个 ExtendedVisualStateManager
可以与任何 FrameworkElement
一起工作;因此,Cinch V2 提供了一个 Blend 触发器,可以与 Cinch SimpleCommand<T1,T2> CommandCompleted
事件一起使用,以利用 ExtendedVisualStateManager
或标准 VisualStateManager
注意:我预计这不会被大量使用,但无论如何,这里是如何使用它的示例。
这就是 CompletedAwareGotoStateCommandTrigger
的完整代码
public class CompletedAwareGoToStateCommandTrigger : TriggerBase<FrameworkElement>
{
#region DPs
#region Command
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICompletionAwareCommand),
typeof(CompletedAwareGoToStateCommandTrigger), null);
/// <summary>
/// Gets or sets the Command property.
/// </summary>
public ICompletionAwareCommand Command
{
get { return (ICompletionAwareCommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
#endregion
#region IsBeingUsedAtRootLevel
#if !SILVERLIGHT
public static readonly DependencyProperty IsBeingUsedAtRootLevelProperty =
DependencyProperty.Register("IsBeingUsedAtRootLevel", typeof(bool),
typeof(CompletedAwareGoToStateCommandTrigger), new UIPropertyMetadata(false));
#else
public static readonly DependencyProperty IsBeingUsedAtRootLevelProperty =
DependencyProperty.Register("IsBeingUsedAtRootLevel", typeof(bool),
typeof(CompletedAwareGoToStateCommandTrigger), new PropertyMetadata(false));
#endif
/// <summary>
/// Gets or sets the IsBeingUsedAtRootLevel property.
/// </summary>
public bool IsBeingUsedAtRootLevel
{
get { return (bool)GetValue(IsBeingUsedAtRootLevelProperty); }
set { SetValue(IsBeingUsedAtRootLevelProperty, value); }
}
#endregion
#endregion
#region Overrides
/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
/// <remarks>
/// Override this to hook up functionality to the AssociatedObject.
/// </remarks>
protected override void OnAttached()
{
base.OnAttached();
this.Command.CommandCompleted += Command_Completed;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.Command.CommandCompleted -= Command_Completed;
}
#endregion
#region Private Methods
private void Command_Completed(object parameter)
{
if (IsBeingUsedAtRootLevel)
{
// Invoke the actions
InvokeActions(parameter);
}
else
{
if (VisualStateManager.GetVisualStateGroups(
this.AssociatedObject).Count > 0)
{
ExtendedVisualStateManager.GoToElementState(
this.AssociatedObject, (string)parameter, true);
}
}
}
#endregion
}
这可能是您的 XAML 的样子(尽管演示应用程序中不包含此内容,但我已在演示应用程序之外尝试过,它确实有效)
<TabControl x:Name="tabControl">
<i:Interaction.Triggers>
<CinchV2:CompletedAwareGoToStateCommandTrigger
IsBeingUsedAtRootLevel="True"
Command="{Binding GoToStateCommand}">
<CinchV2:CommandDrivenGoToStateAction
TargetObject="{Binding ElementName=tabControl}"/>
</CinchV2:CompletedAwareGoToStateCommandTrigger>
</i:Interaction.Triggers>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup">
<VisualState x:Name="GreenState">
</VisualState>
<VisualState x:Name="BlueState">
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
.....rest of code
.....rest of code
.....rest of code
</TabControl>
可以看出,您可以告诉它是否在 VisualTree
的根级别使用,在这种情况下它将使用标准的 GoToStateAction
,否则它将使用 ExtendedVisualStateManager
。
这可能是一些演示 ViewModel 代码的样子
public SimpleCommand<String, String> GoToStateCommand { get; private set; }
GoToStateCommand = new SimpleCommand<String, String>(
(parameter) => { return !string.IsNullOrEmpty(parameter); },
(input)=> {});
//WHICH ALLOWS YOUR TO GO TO A NEW VISUAL STATE AS EASILY AS THIS IN YOUR VIEWMODEL
GoToStateCommand.Execute("BlueState");
EventToCommandTrigger
顾名思义,此触发器提供事件到命令的功能;也就是说,当某个事件发生时,它将触发基于 ViewModel 的 Command
。现在,有些人可能会争辩说,通过简单地使用标准 Blend 操作/触发器,已经有对此的支持。虽然这部分是正确的,但标准的 Blend 缺乏在命令无法执行时禁用消耗 FrameworkElement
的能力。这是 Cinch V2 优于标准 Blend 操作/触发器组合的功能。
所以这是您的 ViewModel 中可能有的内容
//EventToCommand triggered, see the View
ShowActionsCommand = new SimpleCommand<Object, Object>(ExecuteShowActionsCommand);
HideActionsCommand = new SimpleCommand<Object, Object>(ExecuteHideActionsCommand);
然后,在您的视图中,您可能会有这样的内容
<Label Content="Show Actions">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<CinchV2:EventToCommandTrigger
Command="{Binding ShowActionsCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Label>
以下是它在 Expression Blend 中的样子
您所要做的就是输入自定义绑定并输入如上所示的命令绑定,显然将 Command
替换为您的 ViewModel 中的您自己的命令。
如果您不关心 EventArgs
进入 ViewModel,您可以简单地像这样声明 ViewModel 代码
//declare command
public SimpleCommand<Object, Object> ShowActionsCommand { get; private set; }
//initalise command
ShowActionsCommand = new SimpleCommand<Object, Object>(ExecuteShowActionsCommand);
//command handler
private void ExecuteShowActionsCommand(Object args)
{
ShowActionsCommandReversed.Execute(null);
}
但是,如果您想了解导致命令触发的 EventArgs
,您可以在 ViewModel 中执行以下操作
//declare command
public SimpleCommand<Object,EventToCommandArgs>
ViewEventToVMFiredCommand { get; private set; } }
//initalise command
ViewEventToVMFiredCommand =
new SimpleCommand<Object,EventToCommandArgs>(ExecuteViewEventToVMFiredCommand);
//command handler
private void ExecuteViewEventToVMFiredCommand(EventToCommandArgs args)
{
ICommand commandRan = args.CommandRan;
Object o = args.CommandParameter;
EventArgs ea = args.EventArgs;
var sender = args.Sender;
}
可以看出,您可以获得所有相关信息,例如
- 命令参数
EventArgs
sender
(事件源)
注意:我不主张在 ViewModel 中包含 UI 类型对象,因为它更难测试,但有些人喜欢它,所以我确实在 args.EventArgs
/ args.Sender
对象中提供了它,但我不得不说我从未在我编写的任何 ViewModel 代码中做过这种事情。因此,请自行承担风险使用它,当您无法正确测试 ViewModel 时,不要责怪我,我已经警告过您了。
Actions
Cinch V2 提供以下操作
CommandDrivenGoToStateAction
这是一个简单的类,继承自 GoToStateAction
,并期望与 Cinch V2 CompletedAwareGoToStateCommandTrigger
结合使用,后者有一个 CommandCompleted
事件(我们上面讨论过),您可以使用它在 ViewModel 中通过 CommandParameter
提供一个 StateName。
以下是您如何使用此功能的示例
所以你的XAML中会有类似这样的内容
<TabControl x:Name="tabControl">
<i:Interaction.Triggers>
<CinchV2:CompletedAwareGoToStateCommandTrigger
IsBeingUsedAtRootLevel="True"
Command="{Binding GoToStateCommand}">
<CinchV2:CommandDrivenGoToStateAction
TargetObject="{Binding ElementName=tabControl}"/>
</CinchV2:CompletedAwareCommandTrigger>
</i:Interaction.Triggers>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup">
<VisualState x:Name="GreenState">
</VisualState>
<VisualState x:Name="BlueState">
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
.....rest of code
.....rest of code
.....rest of code
</TabControl>
在您的 ViewModel 中会有类似这样的内容;显然,您也可以使用 Cinch V2 提供的 VisualStateManagerService
,但正如我所说,那只有在 VisualStateGroup
直接位于 VisualTree
的根节点下时才有效,因为使用的是标准 VisualStateManager
。
public SimpleCommand<String, String> GoToStateCommand { get; private set; }
GoToStateCommand = new SimpleCommand<String, String>(
(parameter) => { return !string.IsNullOrEmpty(parameter); },
(input)=> {});
//WHICH ALLOWS YOUR TO GO TO A NEW VISUAL STATE AS EASILY AS THIS IN YOUR VIEWMODEL
GoToStateCommand.Execute("BlueState");
按键绑定到命令
注意:在 Cinch V1 中,也支持输入手势触发命令;在 WPF 4 中,这并非微不足道,可以使用以下代码实现
<Window.InputBindings>
<KeyBinding Command="{Binding SomeCommand}" Key="F1" Modifiers="ALT"/>
</Window.InputBindings>
工作区
本节讨论 Cinch V1 (好的,V1 中没有 MeffedMVVM 支持,我指的是 V2 中 V1 的方法) 和 Cinch V2 中对工作区的支持。V1 风格的解决方案和 V2 本身所采用的工作区技术在工作区和设计时数据方面提供了截然不同的支持,因此请仔细阅读。
ViewModel 优先,Cinch V1 风格
现在在 Cinch V1 中,曾经尝试使用 ObservableCollection<ViewModelBase>
并将其与 View 通过某个资源字典中的特定 DataTemplate
结合起来,来实现工作区。您可以使用这个 Cinch V1 文章链接阅读有关此方法的更多信息:CinchIII.aspx#CloseVM。
使用这种方法,我们假设 ViewModel 优先的安排,这种方法的问题在于您并没有真正为 ViewModel 提供它能够获得的最大支持,以便 MeffedMVVM 提供设计时数据。实际上,有一种方法,只是它不是 Cinch V2 中首选的方法。
那么,让我们来看看 Cinch V2/MeffedMVVM 为 Cinch V1 风格的应用程序所使用的 DataTemplate
工作区方法提供了什么样的设计时支持。
ViewModel 设计
ViewModel 优先和 DataTemplate
方法仍然受 Cinch V2 支持,并且 MeffedMVVM 仍然可以用于提供设计时数据,尽管您必须在 ViewModel 上创建 ExportViewModelAttribute
的方式需要告知 ViewModel 期望直接设置为 View 的 DataContext
(例如通过 DataTemplate
),这将是 Cinch V1 风格应用程序中使用 DataTemplate
的 ViewModel 优先方法的情况。
您将 ViewModel 上的 ExportViewModelAttribute
设置如下,并且还实现一个特殊的 MeffedMVVM 接口,称为 IDesignTimeAware
。因此,考虑到所有这些,我们可能会有一个类似于这样的 ViewModel
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cinch;
using MEFedMVVM;
using MEFedMVVM.ViewModelLocator;
using System.Collections.ObjectModel;
using System.ComponentModel;
using MEFedMVVM.Common;
using MEFedMVVM;
using System.ComponentModel.Composition;
namespace WpfApplication1
{
[ExportViewModel("DummyViewModel", true)]
public class DummyViewModel : ViewModelBase, IDesignTimeAware
{
public DummyViewModel()
{
}
private ObservableCollection<string>
data=new ObservableCollection<string>();
static PropertyChangedEventArgs dataChangeArgs =
ObservableHelper.CreateArgs<DummyViewModel>(x => x.Data);
public DummyViewModel()
{
this.DisplayName = "DummyViewModel";
if (!Designer.IsInDesignMode)
{
data.Clear();
for (int i = 0; i < 10; i++)
{
data.Add(string.Format("Runtime {0}", i.ToString()));
}
}
}
public ObservableCollection<string> Data
{
get { return data; }
set
{
if (data == null)
{
data = value;
NotifyPropertyChanged(dataChangeArgs);
}
}
}
#region IDesignTimeAware Members
public void DesignTimeInitialization()
{
data.Clear();
for (int i = 0; i < 10; i++)
{
data.Add(string.Format("DESIGN TIME {0}", i.ToString()));
}
}
#endregion
}
}
重要提示:[ExportViewModel("DummyViewModel", true)]
行告诉 MeffedMVVM 此 ViewModel 是数据感知的,因为它以某种 DataTemplate
方法使用。
另一点需要注意的是,ViewModel 实现了一个名为 IDesignTimeAware
的 MeffedMVVM 接口,该接口允许 MeffedMVVM 调用 DesignTimeInitialization()
方法,以便为此 ViewModel 提供设计时数据。唯一的缺点是您的 ViewModel 现在包含仅在设计时使用的代码。但我认为,这只是很小的代价。
视图设计
难题的另一部分是使用标准 MeffedMVVM 附加 DP,如本 View 所示,它允许 MeffedMVVM 找到 ViewModel 以提供设计时数据
<UserControl x:Class="WpfApplication1.DummyView"
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:MEFed="http:\\www.codeplex.com\MEFedMVVM"
MEFed:ViewModelLocator.ViewModel="DummyViewModel"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<ListBox ItemsSource="{Binding Data}">
</ListBox>
</UserControl>
ViewModel-View 匹配
正如我在本节开头所说,Cinch V1 使用 ViewModel 优先范例,因此,View 是通过使用 DataTemplate
创建的,其中预计某个地方有一个 ObservableCollection<ViewModelBase>
,以及一些 DataTemplate
来匹配特定的 ViewModelBase
实例。例如,此示例显示了一个 View,其中有一个 ObservableCollection<ViewModelBase>
绑定到一个 TabControl
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type local:DummyViewModel}">
<AdornerDecorator>
<local:DummyView />
</AdornerDecorator>
</DataTemplate>
</Window.Resources>
<TabControl ItemsSource="{Binding Path=Workspaces}"
DisplayMemberPath="DisplayName"/>
</Window>
Marlon 在他的博客上更详细地讨论了此功能:http://marlongrech.wordpress.com/2010/05/23/mefedmvvm-v1-0-explained/,但基本上,Marlon 在 MeffedMVVM 中为支持此场景所做的是,他挂钩了 View 的 DataContextChanged
事件,这就是 MeffedMVVM 能够调用 DesignTimeInitialization()
方法以提供设计时数据的方式。
以下是 MeffedMVVM 代码库中的相关代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.Windows;
using MEFedMVVM.Common;
using System.ComponentModel.Composition.Primitives;
namespace MEFedMVVM.ViewModelLocator
{
/// <summary>
/// This is the ViewModel initializer that ViewModel after it is set as datacontext
/// </summary>
public class DataContextAwareViewModelInitializer : BasicViewModelInializer
{
public DataContextAwareViewModelInitializer(MEFedMVVMResolver resolver)
: base (resolver )
{ }
public override void CreateViewModel(Export viewModelContext,
FrameworkElement containerElement)
{
if (!Designer.IsInDesignMode) // if at runtime
{
#if SILVERLIGHT
RoutedEventHandler handler = null;
handler = delegate
{
// it means we have the VM instance now we should inject the services
if (containerElement.DataContext != null)
{
resolver.SatisfyImports(
containerElement.DataContext, containerElement);
}
containerElement.Loaded -= handler;
};
if (containerElement.DataContext == null)
containerElement.Loaded += handler;
else
{
handler(null, default(RoutedEventArgs));
}
#else
DependencyPropertyChangedEventHandler handler = null;
handler = delegate
{
// it means we have the VM instance now we should inject the services
if (containerElement.DataContext != null)
{
resolver.SatisfyImports(
containerElement.DataContext, containerElement);
}
containerElement.DataContextChanged -= handler;
};
if (containerElement.DataContext == null)
// we need to wait until the context is set
containerElement.DataContextChanged += handler;
else // DataContext is already set
{
handler(null, default(DependencyPropertyChangedEventArgs));
}
#endif
}
if(Designer.IsInDesignMode)
{
// this will create the VM and set it as DataContext
base.CreateViewModel(viewModelContext, containerElement );
//if the ViewModel is an IDataContextAware ViewModel
//then we should call the DesignTimeInitialization
var dataContextAwareVM = containerElement.DataContext as IDesignTimeAware;
if (dataContextAwareVM != null)
dataContextAwareVM.DesignTimeInitialization();
}
}
}
}
正如我所说,这就是 Cinch V1 风格的应用程序/ViewModel 优先如何与 MeffedMVVM 愉快地工作,您可以使用 Cinch V1 文章链接阅读有关 DataTemplate
方法的更多信息:CinchIII.aspx#CloseVM。
但我还说这不是 Cinch V2 中提供的方法,所以让我们看看我们可以在 Cinch V2 中做什么。
视图优先:更好的方法(仅支持 WPF,抱歉 Silverlight 用户)
在 Cinch V2 中,我想要实现几件事
- 能够使用 MeffedMVVM 提供的完整的 View 优先设计时数据支持
- 允许将某种上下文数据传递给视图
这些是我提出的要求;听起来很简单,不是吗?那么它们是如何工作的呢?嗯,第一个非常简单,您以前见过,我在第一篇 Cinch V2 文章中讨论过,请阅读此链接 View-ViewModel 解析 以获取更多详细信息;那是标准的 Cinch V2/MeffedMVVM View-ViewModel 解析,所以我不会再重复。
然而,上面的第二点是全新的领域,实际上我非常喜欢。
那么,我们来看一个快速场景
"假设您有一个 TabControl
,它显示客户列表,并且该客户列表有自己的视图(例如 CustomersListView
/CustomerListViewModel
),当您单击列表中的一个客户时,您希望打开一个新视图,该视图在新视图中显示所选客户的详细信息(例如 CustomerEditView
/ CustomerEditViewModel
)。"
这听起来足够简单,您可能会想,哦,我可以使用 Mediator 来实现,但请记住 Mediator 是一个广播器,它广播消息 NotifyColleagues("New Customer Edit", SomeCustomer)
,因此此消息的任何订阅者都必须确定他们是否应该为 CustomerEditView
添加一个新视图。相信我,这并不容易。
因此,我想出了一种变体,它是我在 Cinch V1 中使用 ObservableCollection<T>
和 DataTemplate
提供的;只是这次,我使用 View 优先方法来充分利用 MeffedMVVM。
那么这一切是如何运作的呢?
嗯,分步说明如下
- WPF
ViewModelBase
类包含一个ObservableCollection<WorkspaceData>
,其中每个WorkspaceData
都有一个CloseWorkSpaceCommand
,以确保工作区可以关闭(例如,如果它在TabControl
中)。 - 在需要显示子视图的 View 中,有一个单独的
DataTemplate
与WorkspaceData
类型匹配。 - 在
WorkspaceData
的DataTemplate
中,有一个 Attached DP,它使用绑定的WorkspaceData
来推断应该加载哪个 View。 - 现在了解
WorkspaceData
的 Attached DP 还可以从绑定的WorkspaceData
获取一些额外的上下文信息,并将此数据传递给 View。
我不得不说,我认为这种方法现在为我提供了我想要的所有支持;我可以有 View 优先,我有一种方法可以使用标准代码在我的 ViewModel 中将 View 添加到另一个 View 的区域。我可以将新创建的 View 传递一些上下文数据,最重要的是,由于我的 View 是 View 优先的,我的 ViewModel 可以获得完整的设计时数据支持。
这就是概要,那么代码是什么样子的呢?嗯,让我们从头开始。
工作区:实际的 WorkSpaceData 类
可能首先要看的是实际的 WorkspaceData
代码,它看起来像这样
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace Cinch
{
/// <summary>
/// Workspace data class which can be used within a DataTemplate along
/// with the NavProps DP to manage workspaces
/// </summary>
public partial class WorkspaceData : INotifyPropertyChanged, IDisposable
{
#region Data
private string imagePath;
private string viewLookupKey;
private object dataValue;
private string displayText;
private SimpleCommand<Object, Object> closeWorkSpaceCommand;
private Boolean isCloseable = true;
#endregion
#region Ctor
public WorkspaceData(string imagePath,string viewLookupKey,
object dataValue, string displayText, bool isCloseable)
{
Mediator.Instance.Register(this);
this.ImagePath = imagePath;
this.ViewLookupKey = viewLookupKey;
this.DataValue = dataValue;
this.DisplayText = displayText;
this.IsCloseable = isCloseable;
CloseWorkSpaceCommand = new SimpleCommand<object, object>(
x => true, x => ExecuteCloseWorkSpaceCommand(x));
}
#endregion
#region Custom Closing Event
public event CancelEventHandler WorkspaceTabClosing;
protected void NotifyWorkspaceTabClosing(CancelEventArgs args)
{
CancelEventHandler handler = WorkspaceTabClosing;
if (handler != null)
{
handler(this, args);
}
}
#endregion
#region Command Implememtation
/// <summary>
/// Executes the CloseWorkSpace Command
/// </summary>
private void ExecuteCloseWorkSpaceCommand(object o)
{
CancelEventHandler handler = WorkspaceTabClosing;
if (handler != null &&
WorkspaceTabClosing.GetInvocationList().Count() > 0)
{
CancelEventArgs args = new CancelEventArgs(false);
NotifyWorkspaceTabClosing(args);
if (args.Cancel == false)
{
Mediator.Instance.NotifyColleagues<WorkspaceData>(
"RemoveWorkspaceItem", this);
}
}
else
{
Mediator.Instance.NotifyColleagues<WorkspaceData>(
"RemoveWorkspaceItem", this);
}
}
#endregion
#region Public Properties
/// <summary>
/// The ViewModel that was created for this WorkSpaceData object,
/// if it was used to create a View
/// </summary>
public Object ViewModelInstance { get; set; }
/// <summary>
/// CloseActivePopUpCommand : Close popup command
/// </summary>
public SimpleCommand<Object, Object> CloseWorkSpaceCommand { get; private set; }
/// <summary>
/// Is this workspace a closeable workspace
/// </summary>
static PropertyChangedEventArgs isCloseableArgs =
ObservableHelper.CreateArgs<WorkspaceData>(x => x.IsCloseable);
public Boolean IsCloseable
{
get { return isCloseable; }
set
{
isCloseable = value;
NotifyPropertyChanged(isCloseableArgs);
}
}
/// <summary>
/// True if this workspace has an image
/// </summary>
public bool HasImage
{
get
{
return !string.IsNullOrEmpty(ImagePath);
}
}
/// <summary>
/// ImagePath
/// </summary>
static PropertyChangedEventArgs imagePathArgs =
ObservableHelper.CreateArgs<WorkspaceData>(x => x.ImagePath);
public string ImagePath
{
get { return imagePath; }
set
{
imagePath = value;
NotifyPropertyChanged(imagePathArgs);
}
}
/// <summary>
/// View key lookup name
/// </summary>
static PropertyChangedEventArgs viewLookupKeyArgs =
ObservableHelper.CreateArgs<WorkspaceData>(x => x.ViewLookupKey);
public string ViewLookupKey
{
get { return viewLookupKey; }
set
{
viewLookupKey = value;
NotifyPropertyChanged(viewLookupKeyArgs);
}
}
/// <summary>
/// Workspace context data
/// </summary>
static PropertyChangedEventArgs dataValueArgs =
ObservableHelper.CreateArgs<WorkspaceData>(x => x.DataValue);
public object DataValue
{
get { return dataValue; }
set
{
dataValue = value;
NotifyPropertyChanged(dataValueArgs);
}
}
/// <summary>
/// Workspace display text, is used for Headers controls such as TabControl
/// </summary>
static PropertyChangedEventArgs displayTextArgs =
ObservableHelper.CreateArgs<WorkspaceData>(x => x.DisplayText);
public string DisplayText
{
get { return displayText; }
set
{
displayText = value;
NotifyPropertyChanged(displayTextArgs);
}
}
#endregion
#region Overrides
public override string ToString()
{
return String.Format(
"ViewLookupKey {0}, DisplayText {1}, IsCloseable {2}",
ViewLookupKey, DisplayText, IsCloseable);
}
#endregion
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notify using pre-made PropertyChangedEventArgs
/// </summary>
/// <param name="args"></param>
protected void NotifyPropertyChanged(PropertyChangedEventArgs args)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, args);
}
}
/// <summary>
/// Notify using String property name
/// </summary>
protected void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
#region IDisposable Members
/// <summary>
/// Invoked when this object is being removed from the application
/// and will be subject to garbage collection.
/// </summary>
public void Dispose()
{
Mediator.Instance.Unregister(this);
this.OnDispose();
}
/// <summary>
/// Child classes can override this method to perform
/// clean-up logic, such as removing event handlers.
/// </summary>
protected virtual void OnDispose()
{
}
#if DEBUG
/// <summary>
/// Useful for ensuring that ViewModel objects
/// are properly garbage collected.
/// </summary>
~WorkspaceData()
{
}
#endif
#endregion // IDisposable Members
}
}
如您所见,这是一个非常简单的类,它有一些属性,并且还会(使用 Mediator)通知任何持有这些类实例的 ViewModel 在 CloseWorkSpaceCommand
(可能来自可关闭的 TabItem
)执行时将其删除。
工作区:ViewModelBase 类支持
Cinch WPF ViewModelBase
类有一个 ObservableCollection<WorkspaceData>
,这样任何继承自 Cinch ViewModelBase
类的 ViewModel 都能够管理工作区。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.ComponentModel.Composition;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
namespace Cinch
{
/// <summary>
/// This is a WPF specific partial section of a ViewModelBase
/// </summary>
public abstract partial class ViewModelBase
{
#region Data
/// <summary>
/// Collection of workspaces that this ViewModel manages
/// </summary>
private ObservableCollection<WorkspaceData> views =
new ObservableCollection<WorkspaceData>();
private ICollectionView collectionView;
#endregion
#region Ctor
public ViewModelBase()
{
CloseWorkSpaceCommand = new SimpleCommand<object, object>(
x => true, x => ExecuteCloseWorkSpaceCommand());
Mediator.Instance.RegisterHandler<WorkspaceData>(
"RemoveWorkspaceItem", OnNotifyDataRecieved);
collectionView = CollectionViewSource.GetDefaultView(this.Views);
}
#endregion
#region Mediator Message Sinks
[MediatorMessageSink("RemoveWorkspaceItem")]
void OnNotifyDataRecieved(WorkspaceData workspaceToRemove)
{
if (this.Views.Contains(workspaceToRemove))
{
this.Views.Remove(workspaceToRemove);
}
}
#endregion
#region Public Properties
static PropertyChangedEventArgs viewsArgs =
ObservableHelper.CreateArgs<ViewModelBase>(x => x.Views);
public ObservableCollection<WorkspaceData> Views
{
get { return views; }
set
{
views = value;
NotifyPropertyChanged(viewsArgs);
}
}
#endregion
#region Public Methods
public void SetActiveWorkspace(WorkspaceData viewnav)
{
if (collectionView != null)
collectionView.MoveCurrentTo(viewnav);
}
#endregion
}
}
您还可以看到,有一些代码监听来自 WorkspaceData
的 Mediator 消息,以将其从打开的工作区列表中删除。
工作区:DataTemplate 使一切完美运行
下一步是将一些 WorkspaceData
项添加到 Views
集合。这是来自 Cinch V2 WPF 演示应用程序的一些代码
private void ViewAwareStatusService_ViewLoaded()
{
if (Designer.IsInDesignMode)
return;
String imagePath =
ConfigurationManager.AppSettings["YourImagePath"].ToString();
WorkspaceData workspace1 =
new WorkspaceData(@"/CinchV2DemoWPF;component/Images/imageIcon.png",
"ImageLoaderView", imagePath, "Image View", true);
workspace1.WorkspaceTabClosing += ImageWorkSpace_WorkspaceTabClosing;
WorkspaceData workspace2 =
new WorkspaceData(@"/CinchV2DemoWPF;component/Images/About.png",
"AboutView", null, "About Cinch V2", true);
Views.Add(workspace1);
Views.Add(workspace2);
SetActiveWorkspace(workspace1);
}
//User can choose to cancel closing of workspace here by setting CancelEventArgs
//on the sending WorkspaceData. When we can close the workspace we must also
//unhook the WorkspaceTabClosing to avoid any possible memory leak
private void ImageWorkSpace_WorkspaceTabClosing(object sender, CancelEventArgs e)
{
e.Cancel = false;
CustomDialogResults result =
messageBoxService.ShowYesNo("Are you sure you want to close this tab?",
CustomDialogIcons.Question);
//if user did not want to cancel, keep workspace open
if (result == CustomDialogResults.No)
{
e.Cancel = true;
}
//otherwise close workspace, and make sure to unhook WorkspaceTabClosing event
//to prevent memory leak
else
{
((WorkspaceData)sender).WorkspaceTabClosing -=
ImageWorkSpace_WorkspaceTabClosing;
}
}
看我是如何使用 WorkspaceData
创建两个工作区的,您可能还注意到上面工作区 1 的代码中,我甚至向它传入了一些上下文数据,我们稍后会讲到。
您还可以看到有一个可选的 WorkspaceTabClosing
,我可以使用它进行连接,ViewModel 可能会在那里决定是否真正允许 WorkSpaceData
关闭(这可能是一些代码,或者询问用户,就像我在上面的示例中一样)。
现在我们已经将一些 WorkspaceData
项放在了 Views
集合中,接下来呢?嗯,我们需要提供一个单独的 DataTemplate
来匹配 WorkspaceData
的 Type
。这应该在每个应用程序中只完成一次,因为要使用的模板将始终相同(或者应该如此)。
以下是我在 Cinch V2 WPF 演示应用程序中定义 DataTemplate
的方法
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
xmlns:local="clr-namespace:CinchV2DemoWPF;assembly="
x:Class="CinchV2DemoWPF.MainWindow"
meffed:ViewModelLocator.ViewModel="MainWindowViewModel">
<Window.Resources>
<DataTemplate DataType="{x:Type CinchV2:WorkspaceData}">
<AdornerDecorator>
<Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CinchV2:NavProps.ViewCreator="{Binding}"/>
</AdornerDecorator>
</DataTemplate>
</Window.Resources>
<local:TabControlEx
ItemsSource="{Binding Views}"
CinchV2:NavProps.ShouldHideHostWhenNoItems="true"
DisplayMemberPath="DisplayText">
</local:TabControlEx>
</Window>
在此 XAML 中,请注意我是如何将 TabControl
(嗯,它是一个特殊的 TabControl
,我也将在本文中讨论它)绑定到 Views
集合,并且我提供了一个使用名为 NavProps
的 Attached DP 的 DataTemplate
(稍后会详细介绍)。
*** 非常重要的注意事项 ****
抱歉这张图片,但它引起了您的注意,对吧!!!!!
重要提示 1
我非常、非常强烈建议您使用如上所示的 DataTemplate
。实际上,您必须在 DataTemplate
中提供一个 Border
;否则 Cinch 中的工作区将无法工作。由于 NavProps
DP 期望父级是 Border
,因此您必须提供一个 Border
作为 DataTemplate
中的根容器,所以这非常重要。
重要提示 2
另一点需要注意的是,工作区实际上仅用于绑定到 ItemsControl
,因此您必须坚持使用 ItemsControl
或其任何超类型,例如 TabControl/ListBox
等。基本上,使用 ListBox
,您应该能够创建任何东西,从单个项目到多个项目;请记住,使用 ListBox
,您可以交换 ItemsPanelTemplate
以使用任何标准布局容器,例如 Grid/Canvas
等,因此我相信您可以使用 ItemsControl
或其任何超类型做任何事情。未能使用 ItemsControl
将导致工作区不工作。
你必须遵守这两点……如果你不遵守,将导致工作区无法工作,所以只要遵守这些注意事项,你就应该没问题。
工作区:用于解析视图的 NavProps 附加 DP
回想一下,我们之前简要提到了一个名为 NavProps
的附加 DP。那么,它为我们做了什么呢?实际上有两件事。实际上有两个附加 DP
ShouldHideHostWhenNoItems
附加 DP:当没有更多可显示项目时,可用于隐藏ItemsControl
主机。ViewCreator
附加 DP:用于绑定WorkspaceData
对象,当它更改时,它会检查绑定的WorkspaceData.ViewLookupKey
并根据该键创建一个新 View。
ShouldHideHostWhenNoItems 附加 DP
这其实很简单,它基本上只是一个布尔值,可以设置在您的 ItemsControl
上,以便在其中没有更多可显示项时将其隐藏。当您允许用户关闭工作区项(例如可关闭的选项卡)时,这可能最有用。
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
xmlns:local="clr-namespace:CinchV2DemoWPF;assembly="
x:Class="CinchV2DemoWPF.MainWindow"
meffed:ViewModelLocator.ViewModel="MainWindowViewModel">
<local:TabControlEx
ItemsSource="{Binding Views}"
CinchV2:NavProps.ShouldHideHostWhenNoItems="true"
DisplayMemberPath="DisplayText">
</local:TabControlEx>
</Window>
ViewCreator 附加 DP
这是 NavProps
类中的第二个附加 DP。正如我所说,这个负责根据在数据绑定 WorkspaceData.ViewLookupKey
上设置的一些神奇字符串实际创建新的 View。这段代码看起来像这样
/// <summary>
/// This DP is used to create the actual workspace View based on the value of the
/// bound WorkspaceData.ViewType
/// </summary>
#region ViewCreator
/// <summary>
/// ViewCreator Attached Dependency Property
/// </summary>
public static readonly DependencyProperty ViewCreatorProperty =
DependencyProperty.RegisterAttached("ViewCreator",
typeof(WorkspaceData), typeof(NavProps),
new FrameworkPropertyMetadata((WorkspaceData)null,
new PropertyChangedCallback(OnViewCreatorChanged)));
/// <summary>
/// Gets the ViewCreator property.
/// </summary>
public static WorkspaceData GetViewCreator(DependencyObject d)
{
return (WorkspaceData)d.GetValue(ViewCreatorProperty);
}
/// <summary>
/// Sets the ViewCreator property.
/// </summary>
public static void SetViewCreator(DependencyObject d, WorkspaceData value)
{
d.SetValue(ViewCreatorProperty, value);
}
/// <summary>
/// Handles changes to the ViewCreator property.
/// </summary>
private static void OnViewCreatorChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
ItemsControl itemsControl = null;
if (e.NewValue == null)
{
itemsControl = TreeHelper.TryFindParent<ItemsControl>(d);
bool shouldHideHostWhenNoItems =
(bool)itemsControl.GetValue(NavProps.ShouldHideHostWhenNoItemsProperty);
if (shouldHideHostWhenNoItems)
{
if (itemsControl != null)
itemsControl.Visibility = Visibility.Collapsed;
}
return;
}
Border contPresenter = (Border)d;
WorkspaceData viewNavData = (WorkspaceData)e.NewValue;
var theView = ViewResolver.CreateView(viewNavData.ViewLookupKey);
viewNavData.ViewModelInstance = ((FrameworkElement)theView).DataContext;
IWorkSpaceAware dataAwareView = theView as IWorkSpaceAware;
if (dataAwareView == null)
{
throw new InvalidOperationException(
"NavProps attached property is only designed to work " +
" with Views that implement the IWorkSpaceAware interface");
}
else
{
dataAwareView.WorkSpaceContextualData = viewNavData;
contPresenter.Child = (UIElement)dataAwareView;
}
itemsControl = TreeHelper.TryFindParent<ItemsControl>(d);
if (itemsControl != null)
itemsControl.Visibility = Visibility.Visible;
}
#endregion
回想一下之前的 DataTemplate
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
xmlns:local="clr-namespace:CinchV2DemoWPF;assembly="
x:Class="CinchV2DemoWPF.MainWindow"
meffed:ViewModelLocator.ViewModel="MainWindowViewModel">
<Window.Resources>
<DataTemplate DataType="{x:Type CinchV2:WorkspaceData}">
<AdornerDecorator>
<Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CinchV2:NavProps.ViewCreator="{Binding}"/>
</AdornerDecorator>
</DataTemplate>
</Window.Resources>
<local:TabControlEx
ItemsSource="{Binding Views}"
CinchV2:NavProps.ShouldHideHostWhenNoItems="true"
DisplayMemberPath="DisplayText">
</local:TabControlEx>
</Window>
现在您可能会注意到有一个名为 ViewResolver
的类(var theView = ViewResolver.CreateView(viewNavData.ViewLookupKey);
),那么这个 ViewResolver
如何知道如何处理一个字符串,以及它如何从中创建一个新的 Window
呢?简单来说,ViewResolver
只是一个 Dictionary<string,type>
,它使用 Activator.CreateInstance
来创建一个与字符串查找键(是的,来自绑定的 WorkspaceData
)匹配的类型的新实例。
然后,它从 View 中获取 DataContext
(MEF 提供),并将其存储回 WorkspaceData
对象中,这样创建 WorkspaceData
对象的代码将有一个指向新创建的 ViewModel 的链接。
然后,它将获取的 View 转换为 IWorkSpaceAware
,并设置 dataAwareView.WorkSpaceContextualData
属性,传入由 WorkspaceData
提供的上下文数据。
在演示应用程序中,我使用 WorkspaceData
通过 View 实现的 IWorkSpaceAware
接口将目录路径传递给 ImageLoaderView
。接下来发生的是 MeffedMVVM 创建 ViewModel,并且 WPF 演示应用程序的 ViewModel 恰好使用了 IViewAwareStatus
服务,因此我们挂钩到该服务的加载事件,然后在 ViewModel 中执行以下操作。
private void ViewAwareStatusService_ViewLoaded()
{
if (!Designer.IsInDesignMode)
{
var view = viewAwareStatusService.View;
IWorkSpaceAware workspaceData = (IWorkSpaceAware)view;
DirectoryName = (String)workspaceData.WorkSpaceContextualData.DataValue;
}
LoadImages(DirectoryName);
}
这就是我们如何将上下文数据从工作区获取到 View 中,并通过 View(使用 View 上的 IWorkspaceAware
接口)获取到 ViewModel 中的方法。
但是,ViewResolvers Dictionary<string,type>
是如何最初填充的呢?在 Cinch V2 WPF 演示应用程序中,当您调用 CinchBootstrapper
反射性地遍历 IEnumerable<Assembly>
以尝试查找任何使用特殊 Cinch ViewnameToViewLookupKeyMetadata
属性修饰的 View(UserControls)时,这会在 App.xaml.cs 中发生。
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;
using System.ComponentModel.Composition.Hosting;
using Cinch;
using System.Reflection;
using MEFedMVVM.ViewModelLocator;
namespace CinchV2DemoWPF
{
public partial class App : Application
{
public App()
{
CinchBootStrapper.Initialise(
new List<Assembly> { typeof(App).Assembly });
InitializeComponent();
}
}
}
以下是一个带有 ViewnameToViewLookupKeyMetadata
的示例视图
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Cinch;
using System.Diagnostics;
namespace CinchV2DemoWPF
{
[ViewnameToViewLookupKeyMetadata("ImageLoaderView", typeof(ImageLoaderView))]
public partial class ImageLoaderView : UserControl, IWorkSpaceAware
{
}
}
这就是 ViewResolvers Dictionary<string,type>
填充的方式,以便 Attached DP 调用它来创建与 DataTemplate
中绑定的 WorkspaceData
所请求的 View 类型匹配的 View。
工作区:特别说明
这一切都很棒,但不幸的是,WPF 在我们的路径中抛出了一些奇怪的东西,以 TabControl
的形式出现。这是一个非常难用的控件。你们有多少人知道在 WPF 中 TabControl
的 VisualTree
只保留选中项在 VisualTree
中。
这听起来很糟糕吗?不,再想想(尽管这只在使用 DataTemplate
时才是个问题,直接的 TabItem
/ View 组合是没问题的)。所以我们有几个 View 使用 MeffedMVVM 在 TabControl
中创建一个 ViewModel。然后我们切换选项卡,你猜怎么着?View 被销毁了,当我们返回到前一个 TabItem
时,由于我们使用 View 优先和 MeffedMVVM,为 View 创建了一个新的 ViewModel。
这有点乱,你不觉得吗?嗯,至少我是这么认为。
幸运的是,帮助就在眼前。我早就知道这一点,并为 WPF 精心制作了一个特殊的 TabControl
,它在选择更改时不会销毁 VisualTree
,而是将所有项目保留在内存中并更改它们的 Visibility
。
这需要您自己的 WPF 项目中包含两件事
它看起来像这样
using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace CinchV2DemoWPF
{
[TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
public class TabControlEx : TabControl
{
private Panel _itemsHolder = null;
public TabControlEx()
: base()
{
// this is necessary so that we get
// the initial databound selected item
this.ItemContainerGenerator.StatusChanged +=
ItemContainerGenerator_StatusChanged;
}
/// <summary>
/// if containers are done, generate the selected item
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (this.ItemContainerGenerator.Status ==
GeneratorStatus.ContainersGenerated)
{
this.ItemContainerGenerator.StatusChanged -=
ItemContainerGenerator_StatusChanged;
UpdateSelectedItem();
}
}
/// <summary>
/// get the ItemsHolder and generate any children
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel;
UpdateSelectedItem();
}
/// <summary>
/// when the items change we remove any generated panel
/// children and add any new ones as necessary
/// </summary>
/// <param name="e"></param>
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (_itemsHolder == null)
{
return;
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Reset:
_itemsHolder.Children.Clear();
break;
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Remove:
if (e.OldItems != null)
{
foreach (var item in e.OldItems)
{
ContentPresenter cp = FindChildContentPresenter(item);
if (cp != null)
{
_itemsHolder.Children.Remove(cp);
}
}
}
// don't do anything with new items because we don't want to
// create visuals that aren't being shown
UpdateSelectedItem();
break;
case NotifyCollectionChangedAction.Replace:
throw new NotImplementedException("Replace not implemented yet");
}
}
/// <summary>
/// update the visible child in the ItemsHolder
/// </summary>
/// <param name="e"></param>
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
UpdateSelectedItem();
}
/// <summary>
/// generate a ContentPresenter for the selected item
/// </summary>
void UpdateSelectedItem()
{
if (_itemsHolder == null)
{
return;
}
// generate a ContentPresenter if necessary
TabItem item = GetSelectedTabItem();
if (item != null)
{
CreateChildContentPresenter(item);
}
// show the right child
foreach (ContentPresenter child in _itemsHolder.Children)
{
child.Visibility = ((child.Tag as TabItem).IsSelected) ?
Visibility.Visible : Visibility.Collapsed;
}
}
/// <summary>
/// create the child ContentPresenter
/// for the given item (could be data or a TabItem)
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
ContentPresenter CreateChildContentPresenter(object item)
{
if (item == null)
{
return null;
}
ContentPresenter cp = FindChildContentPresenter(item);
if (cp != null)
{
return cp;
}
// the actual child to be added. cp.Tag is a reference to the TabItem
cp = new ContentPresenter();
cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
cp.ContentTemplate = this.SelectedContentTemplate;
cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
cp.ContentStringFormat = this.SelectedContentStringFormat;
cp.Visibility = Visibility.Collapsed;
cp.Tag = (item is TabItem) ? item :
(this.ItemContainerGenerator.ContainerFromItem(item));
_itemsHolder.Children.Add(cp);
return cp;
}
/// <summary>
/// Find the CP for the given object.
/// data could be a TabItem or a piece of data
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
ContentPresenter FindChildContentPresenter(object data)
{
if (data is TabItem)
{
data = (data as TabItem).Content;
}
if (data == null)
{
return null;
}
if (_itemsHolder == null)
{
return null;
}
foreach (ContentPresenter cp in _itemsHolder.Children)
{
if (cp.Content == data)
{
return cp;
}
}
return null;
}
/// <summary>
/// copied from TabControl; wish it were protected
/// in that class instead of private
/// </summary>
/// <returns></returns>
protected TabItem GetSelectedTabItem()
{
object selectedItem = base.SelectedItem;
if (selectedItem == null)
{
return null;
}
TabItem item = selectedItem as TabItem;
if (item == null)
{
item = base.ItemContainerGenerator.ContainerFromIndex(
base.SelectedIndex) as TabItem;
}
return item;
}
}
}
但您还需要使用此 Style
才能使 TabControlEx
工作
<Style x:Key="TabControlStyleVerticalTabs" TargetType="{x:Type local:TabControlEx}">
<Setter Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Background" Value="White"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TabControlEx}">
<DockPanel >
<TabPanel x:Name="tabpanel" Margin="0,15,0,0"
Visibility="Visible"
DockPanel.Dock="Left"
KeyboardNavigation.TabIndex="1"
IsItemsHost="True" />
<Border CornerRadius="10,0,0,10"
Margin="0,5,0,5"
Background="{TemplateBinding Background}">
<Grid DockPanel.Dock="Bottom" Margin="10,0,0,0"
Background="{TemplateBinding Background}"
x:Name="PART_ItemsHolder" />
</Border>
</DockPanel>
<!-- no content presenter -->
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground"
Value="{DynamicResource {x:Static
SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
为了完整起见,以下是我在 WPF 演示应用程序中实现可关闭 TabItem
的方法,其中 PART_Close
Button
绑定到用于创建应用于 TabItem
的 DataTemplate
的 WorkspaceData
。
<Style x:Key="TabItemStyleVerticalTabs" TargetType="{x:Type TabItem}">
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid SnapsToDevicePixels="true">
<Border x:Name="Bd" BorderThickness="0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="2"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid x:Name="grid" Margin="0"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<StackPanel Orientation="Horizontal" Margin="15,5,15,5" >
<Button x:Name="PART_Close"
HorizontalAlignment="Left" Margin="2,0,2,0"
VerticalAlignment="Center" Width="16"
Height="16"
Command="{Binding Path=CloseWorkSpaceCommand}"
Visibility="{Binding IsCloseable,
Converter={StaticResource boolToVisConv},
ConverterParameter=True}"
Focusable="False"
Style="{DynamicResource CloseableTabItemButtonStyle}"
ToolTip="Close Tab">
<Path x:Name="Path" Stretch="Fill"
StrokeThickness="0.5"
Stroke="{DynamicResource closeTabCrossStroke}"
Fill="Black"
Data="F1 M 2.28484e-007,
1.33331L 1.33333,0L 4.00001,
2.66669L 6.66667,
6.10352e-005L 8,1.33331L 5.33334,
4L 8,6.66669L 6.66667,8L 4,
5.33331L 1.33333,8L 1.086e-007,
6.66669L 2.66667,4L 2.28484e-007,1.33331 Z "
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
</Button>
<Image Source="{Binding ImagePath}" Width="32"
Height="32" Margin="2,0,2,0"
Visibility="{Binding HasImage,
Converter={StaticResource boolToVisConv},
ConverterParameter=True}"
VerticalAlignment="Center"/>
<Label x:Name="lbl" Margin="2,0,2,0"
FontSize="12"
FontWeight="Bold"
Content="{Binding Path=DisplayText}"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
</StackPanel>
<Label x:Name="lblArrow" FontFamily="Wingdings 3"
Content="t" FontSize="16"
Foreground="White" Margin="0,0,-9,0"
Opacity="0"
VerticalAlignment="Center"
VerticalContentAlignment="Center"
HorizontalAlignment="Right"
HorizontalContentAlignment="Right"/>
</Grid>
<Rectangle x:Name="rectShine" Grid.Row="1"
Opacity="0.5" Fill="#ff656565"
StrokeThickness="0" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" Height="2" />
</Grid>
</Border>
</Grid>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="true"/>
</MultiTrigger.Conditions>
<Setter Property="Panel.ZIndex" Value="1"/>
<Setter Property="Background" TargetName="Bd"
Value="{StaticResource selectedBrush}"/>
<Setter Property="Background" TargetName="grid"
Value="{StaticResource selectedGradientGlow}"/>
<Setter Property="Opacity" TargetName="lblArrow" Value="1"/>
<Setter Property="Height" TargetName="rectShine" Value="2"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="false"/>
<Condition Property="IsMouseOver" Value="true"/>
</MultiTrigger.Conditions>
<Setter Property="Panel.ZIndex" Value="1"/>
<Setter Property="Background" TargetName="Bd"
Value="{StaticResource nonSelectedBrush}"/>
<Setter Property="Background"
TargetName="grid" Value="Transparent"/>
<Setter Property="Opacity"
TargetName="lblArrow" Value="0"/>
<Setter Property="Height"
TargetName="rectShine" Value="2"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="false"/>
<Condition Property="IsMouseOver" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="Height" TargetName="rectShine" Value="0"/>
<Setter Property="Foreground" TargetName="lbl" Value="White"/>
<Setter Property="Fill" TargetName="Path" Value="White"/>
</MultiTrigger>
<Trigger Property="TabStripPlacement" Value="Right">
<Setter Property="Content" TargetName="lblArrow" Value="u"/>
<Setter Property="Margin" TargetName="lblArrow" Value="-9,0,0,0"/>
<Setter Property="HorizontalAlignment"
TargetName="lblArrow" Value="Left"/>
<Setter Property="HorizontalContentAlignment"
TargetName="lblArrow" Value="Left"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
正如我所说,Cinch V2 中的工作区支持仅适用于 WPF,我将不为 Silverlight 提供它,原因有几个
- Silverlight 的
TabControl
缺少一些我需要它像 WPF 中一样工作的重写和通用内部机制。 - 在 Silverlight 中,我认为更多地倾向于使用
NavigationFrame
等来提供导航,我认为这是一个好主意。我认为桌面应用程序应该看起来和工作像桌面应用程序,而 Web 应用程序应该看起来像 Web 应用程序,这暗示着(至少对我来说)TabControl
类的功能只属于桌面……但这只是我的意见。如果您不同意,可以实现类似 WPF 版本的方案,并查看 WPF 版本的IViewAwareStatus
服务实现,这与 Silverlight 版本不同。WPF 版本到处使用WeakEvent
s/WeakReference
。
额外的线程辅助程序
Cinch V1 已经有一些 Dispatcher
相关辅助程序和扩展方法。如果您错过了 Cinch V1 中的一些实用程序,这里是它们的简要说明
BackgroundTaskManager
:BackgroundWorker
的一个小包装器DispatcherExtensions
:一些不错的Dispatcher
扩展(仅 WPF)DispatcherNotifiedObservableCollection<T>
:将封送到 UI 线程的ObservableCollection<T>
ApplicationHelper
:提供DoEvents()
(仅 WPF)
我决定在 Cinch V2 中再包含一个,它是我从 WPF 门徒兼好友 Daniel Vaughan 那里借来的。原始代码可在 Daniel 的文章 https://codeproject.org.cn/KB/silverlight/Mtvt.aspx 中找到。
它基本上是一个 UI 同步上下文,类似于 WinForms 中找到的那个,但它是为 WPF 和 Silverlight 量身定制的。奇怪的是,标准 WPF/Silverlight 基类中没有这样的对象,真是可惜。
这个类有一个接口,这样如果你想创建一个模拟或测试替身,你可以。这是接口
using System;
using System.Threading;
using System.Windows.Threading;
/// <summary>
/// This class was obtained from Daniel Vaughan (a fellow WPF Discple)
/// https://codeproject.org.cn/KB/silverlight/Mtvt.aspx
/// </summary>
namespace Cinch
{
/// <summary>
/// SynchronizationContext interface that provides
/// various thread marshalling calls to be done
/// </summary>
public interface ISynchronizationContext
{
bool InvokeRequired { get; }
void Initialize();
void Initialize(Dispatcher dispatcher);
void InvokeAndBlockUntilCompletion(Action action);
void InvokeAndBlockUntilCompletion(SendOrPostCallback callback, object state);
void InvokeWithoutBlocking(Action action);
void InvokeWithoutBlocking(SendOrPostCallback callback, object state);
}
}
以下是实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Threading;
/// <summary>
/// This class was obtained from Daniel Vaughan (a fellow WPF Discple)
/// https://codeproject.org.cn/KB/silverlight/Mtvt.aspx
/// </summary>
namespace Cinch
{
/// <summary>
/// Singleton class providing the default implementation
/// for the <see cref="ISynchronizationContext"/>,
/// specifically for the UI thread.
/// </summary>
public partial class UISynchronizationContext : ISynchronizationContext
{
#region Data
private DispatcherSynchronizationContext context;
private Dispatcher dispatcher;
private readonly object initializationLock = new object();
#endregion
#region Singleton implementation
static readonly UISynchronizationContext instance =
new UISynchronizationContext();
/// <summary>
/// Gets the singleton instance.
/// </summary>
/// <value>The singleton instance.</value>
public static ISynchronizationContext Instance
{
get
{
return instance;
}
}
#endregion
#region Private Methods
private void EnsureInitialized()
{
if (dispatcher != null && context != null)
{
return;
}
lock (initializationLock)
{
if (dispatcher != null && context != null)
{
return;
}
try
{
#if SILVERLIGHT
dispatcher = System.Windows.Deployment.Current.Dispatcher;
#else
dispatcher = Dispatcher.CurrentDispatcher;
#endif
context = new DispatcherSynchronizationContext(dispatcher);
}
catch (InvalidOperationException)
{
throw new Exception("Initialised called from non-UI thread.");
}
}
}
#endregion
#region ISynchronizationContext Methods
public void Initialize()
{
EnsureInitialized();
}
public void Initialize(Dispatcher dispatcher)
{
ArgumentValidator.AssertNotNull(dispatcher, "dispatcher");
lock (initializationLock)
{
this.dispatcher = dispatcher;
context = new DispatcherSynchronizationContext(dispatcher);
}
}
public void InvokeWithoutBlocking(
SendOrPostCallback callback, object state)
{
ArgumentValidator.AssertNotNull(callback, "callback");
EnsureInitialized();
context.Post(callback, state);
}
public void InvokeWithoutBlocking(Action action)
{
ArgumentValidator.AssertNotNull(action, "action");
EnsureInitialized();
context.Post(state => action(), null);
}
public void InvokeAndBlockUntilCompletion(
SendOrPostCallback callback, object state)
{
ArgumentValidator.AssertNotNull(callback, "callback");
EnsureInitialized();
context.Send(callback, state);
}
public void InvokeAndBlockUntilCompletion(Action action)
{
ArgumentValidator.AssertNotNull(action, "action");
EnsureInitialized();
if (dispatcher.CheckAccess())
{
action();
}
else
{
context.Send(delegate { action(); }, null);
}
}
public bool InvokeRequired
{
get
{
EnsureInitialized();
return !dispatcher.CheckAccess();
}
}
#endregion
}
}
额外的实用程序
Cinch V1 已经有一些我添加了的方便的实用程序。如果您错过了 Cinch V1 中的一些实用程序,这里是它们的简要说明
ObservableHelper
:提供一个获取表达式树中属性名字符串的小类PropertyObserver
:一个不错的弱引用INotifyPropertyChanged
监听器
无论如何,对于 Cinch V2,我还包含了这些额外的实用程序
PropertyChangedEventManager
这仅适用于 Silverlight。
由于 Silverlight 没有 PropertyChangedEventManager
,我想我会提供一个来填补空白(WPF 当然有这个类)。事实上,当我说我想提供一个时,我真正的意思是我是从 WPF 门徒 Pete O'Hanlon 那里偷来的。所以,如果您发现您在 Silverlight 中需要 PropertyChangedEventManager
,请不要害怕,它就在这里。
ArgumentValidator
这是一个简单的类,提供许多可用于验证方法参数的方法。
BindingEvaluator(仅 WPF)
这仅适用于 WPF。
我发现这个类有时非常有用。基本上,它是一个非常简单的类,允许您查找 Binding
的值;以下是完整的代码列表
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;
namespace Cinch
{
/// <summary>
/// Use this evaluator when you do not know the return type expected.
/// The return type will always be Object, and you will have to deal with
/// that outside of this class
/// </summary>
/// <remarks>
/// Recommended usage:
/// <code>
/// TextBox positionedTextBox = new TextBox();
/// Binding positionBinding = new Binding("Minute");
/// positionBinding.Source = System.DateTime.Now;
/// positionedTextBox.SetBinding(Canvas.TopProperty, positionBinding);
///
/// //Use GenericBindingEvaluator to get Bound Value
/// BindingEvaluator be = new BindingEvaluator();
/// Object x = be.GetBoundValue(positionBinding);
///
/// </code>
/// </remarks>
public class BindingEvaluator : DependencyObject
{
#region DPs
/// <summary>
/// Dummy internal DP, to bind and get value from
/// </summary>
public static readonly DependencyProperty DummyProperty =
DependencyProperty.Register(
"Dummy", typeof(Object), typeof(DependencyObject),
new UIPropertyMetadata(null));
public Object Dummy
{
get { return (Object)GetValue(DummyProperty); }
set { SetValue(DummyProperty, value); }
}
#endregion
#region Public Methods
/// <summary>
/// Evaluate the binding
/// </summary>
/// <param name="bindingToEvaluate">The BindingBase to get the value of</param>
/// <returns>The result of the BindingBase</returns>
public Object GetBoundValue(BindingBase bindingToEvaluate)
{
BindingOperations.SetBinding(this,
BindingEvaluator.DummyProperty, bindingToEvaluate);
return this.Dummy;
}
#endregion
}
/// <summary>
/// Use this evaluator when you know the return type expected
/// </summary>
/// <typeparam name="T">The return type expected from the Binding</typeparam>
/// <remarks>
/// Recommended usage:
/// <code>
/// TextBox positionedTextBox = new TextBox();
/// Binding positionBinding = new Binding("Minute");
/// positionBinding.Source = System.DateTime.Now;
/// positionedTextBox.SetBinding(Canvas.TopProperty, positionBinding);
///
/// //Use GenericBindingEvaluator to get Bound Value
/// GenericBindingEvaluator<Int32> be =
/// new GenericBindingEvaluator<Int32>();
/// Int32 x = be.GetBoundValue(positionBinding);
///
/// </code>
/// </remarks>
public class GenericBindingEvaluator<T> : DependencyObject
{
#region DPs
/// <summary>
/// Dummy internal DP, to bind and get value from
/// </summary>
public static readonly DependencyProperty DummyProperty =
DependencyProperty.Register(
"Dummy", typeof(T), typeof(DependencyObject),
new UIPropertyMetadata(null));
public T Dummy
{
get { return (T)GetValue(DummyProperty); }
set { SetValue(DummyProperty, value); }
}
#endregion
#region Public Methods
/// <summary>
/// Evaluate the binding
/// </summary>
/// <param name="bindingToEvaluate">The BindingBase to get the value of</param>
/// <returns>The result of the BindingBase</returns>
public T GetBoundValue(BindingBase bindingToEvaluate)
{
BindingOperations.SetBinding(this,
GenericBindingEvaluator<T>.DummyProperty, bindingToEvaluate);
return this.Dummy;
}
#endregion
}
}
请参阅上面代码块中推荐的用法注释,以了解如何使用此类别。
ObservableDictionary<TKey, TValue>(仅 WPF)
这仅适用于 WPF。
这是我直接从 Dr. WPF 那里偷来的,它是一个写得非常好的类,基本上是一个可绑定的 ObservableDictionary
,正如它所说的那样。
TreeHelper(仅限WPF)
这仅适用于 WPF。
使用 WPF 时,您将使用 VisualTree
,因此有一些辅助工具来帮助完成繁琐的工作会很有用。WPF 门徒 Philip Sumi 有一个不错的类,名为 TreeHelper
,我已经将其包含在 Cinch V2 中,它提供了各种方法,例如
public static T TryFindParent<T>(this DependencyObject child) where T : DependencyObject
public static DependencyObject GetParentObject(this DependencyObject child)
public static IEnumerable<T> FindChildren<T>(this DependencyObject source) where T : DependencyObject
public static IEnumerable<DependencyObject> GetChildObjects(this DependencyObject parent)
public static T TryFindFromPoint<T>(UIElement reference, Point point) where T : DependencyObject
它确实是一个非常有用的类。
暂时就到这里
如果您喜欢这篇文章,并且觉得它对您有帮助,能否请您通过留下投票/评论来表示支持?
与之前一样,如果您有任何与 MEF 相关的深入问题,您应该直接向 Marlon Grech 提出,可以通过他的博客 C# Disciples,或通过 MefedMVVM CodePlex 网站;任何其他 Cinch V2 问题将在下一篇 Cinch V2 文章中得到解答。