我正在尝试尽可能地遵循WPF应用程序中的MVVM模式,主要是为了能够为我的ViewModel逻辑创建单元测试。
在大多数情况下,ViewModel属性与可视元素属性之间的数据绑定工作正常且很容易。但有时我遇到的情况是,我无法看到明显和直接的方式,而从代码隐藏访问和操作控件的解决方案非常容易。
以下是我的意思示例:将文本片段插入当前插入位置的TextBox
由于CaretIndex
不是依赖项属性,因此无法直接绑定到ViewModel的属性。 Here是通过创建依赖项属性来解决此限制的解决方案。 here是在代码隐藏中执行此操作的解决方案。在这种情况下,我更喜欢代码隐藏方式。我最近遇到的另一个问题是将动态的列集合绑定到WPF数据网格。在代码隐藏中编程非常简单明了。但对于MVVM友好的数据绑定方法,我只能在几个博客中找到解决方法,这些博客对我来说都很复杂,并且在一个或另一个方面有各种限制。
我不想不惜一切代价保持MVVM架构清除代码隐藏逻辑。如果工作量太大,MVVM友好的解决方案需要大量我不完全理解的代码(我仍然是WPF初学者)并且太耗时我更喜欢代码隐藏解决方案并牺牲我的应用程序的一些部分的自动可测试性。
出于上述实际原因,我现在正在寻找“模式”来在应用程序中控制使用代码隐藏,而不会破坏MVVM架构或者不会破坏它。
到目前为止,我已经找到并测试了两种解决方案。我将用Caret Position示例绘制粗略的草图:
解决方案1)通过抽象界面为ViewModel提供对View的引用
我将有一个接口,其中包含将由视图实现的方法:
public interface IView
{
void InsertTextAtCaretPosition(string text);
}
public partial class View : UserControl, IView
{
public View()
{
InitializeComponent();
}
// Interface implementation
public void InsertTextAtCaretPosition(string text)
{
MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, text);
}
}
将此接口注入ViewModel
public class ViewModel : ViewModelBase
{
private readonly IView _view;
public ViewModel(IView view)
{
_view = view;
}
}
通过接口方法
从ViewModel的命令处理程序执行代码隐藏public ICommand InsertCommand { get; private set; }
// Bound for instance to a button command
// Command handler
private void InsertText(string text)
{
_view.InsertTextAtCaretPosition(text);
}
要创建View-ViewModel对,我将使用依赖注入实例化具体的View并将其注入ViewModel。
解决方案2)通过事件执行代码隐藏方法
ViewModel是特殊事件的发布者,命令处理程序会引发这些事件
public class ViewModel : ViewModelBase
{
public ViewModel()
{
}
public event InsertTextEventHandler InsertTextEvent;
// Command handler
private void InsertText(string text)
{
InsertTextEventHandler handler = InsertTextEvent;
if (handler != null)
handler(this, new InsertTextEventArgs(text));
}
}
View订阅了这些活动
public partial class View : UserControl
{
public View()
{
InitializeComponent();
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
ViewModel viewModel = DataContext as ViewModel;
if (viewModel != null)
viewModel.InsertTextEvent += OnInsertTextEvent;
}
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
{
ViewModel viewModel = DataContext as ViewModel;
if (viewModel != null)
viewModel.InsertTextEvent -= OnInsertTextEvent;
}
private void OnInsertTextEvent(object sender, InsertTextEventArgs e)
{
MyTextBox.Text = MyTextBox.Text.Insert(MyTextBox.CaretIndex, e.Text);
}
}
我不确定Loaded
的{{1}}和Unloaded
事件是否是订阅和取消订阅活动的好地方,但我在测试期间找不到问题。
我在两个简单的例子中测试了这两种方法,它们似乎都有效。现在我的问题是:
您认为哪种方法更可取?我可能没有看到其中一种解决方案的任何好处或缺点吗?
您是否看到(也许可以练习)其他解决方案?
提前感谢您的反馈!
答案 0 :(得分:22)
特别针对此问题
此特定案例的最简单解决方案是添加执行此操作的附加属性或行为。在mvvm中,行为可能是大多数这些富有gui不支持的案例的银弹。
至于一般情况
ViewModel永远不应该在任何情况下知道视图,甚至不应该知道IView。在MVVM中它“始终查找”,这意味着View可以看到@ VM,而VM可以查看模型。从来没有反过来。 这样可以创建更好的可维护性,因为这样ViewModel不会做两件事(逻辑和gui的充实),但只有一件事。这就是MVVM优于任何先前MV *模式的地方。
我也会尝试避免View以耦合的方式依赖ViewModel。这会产生丑陋的代码,并且两个类之间存在可分解的依赖关系,但有时这会更加务实,如您所说。 更漂亮的方法是从ViewModel向View发送松散消息(例如MVVMLight中的Messenger或者Prism中的EventAggregator),因此两者之间没有强烈的依赖关系。有些人认为这更好,尽管IMO仍然是依赖。
在某些情况下,在View中编写代码是正常的,这可能是其中一种情况。你可以使用附加行为来实现完美的解决方案,但原则很重要,就像你问的那样。
当你需要非常丰富的GUI或者没有正确的属性来绑定时,MVVM是有问题的。 在这些情况下,您将采用以下三种方法之一:
所有这些方式都是合法的,但我已根据您应该首先采用的方式对它们进行了排序。
总结
你必须保留在MVVM中最重要的不是保持代码隐藏,而是保持所有逻辑和放大器。数据到ViewModel,其中View必须只包含与View相关的代码。建筑师告诉你不要写代码的原因只是因为它是一个滑坡。你开始写一些小东西,最后你会在View中做逻辑工作或维护应用程序状态,这是最大的禁忌。
快乐的MVVMing:)
答案 1 :(得分:7)
开发WPF应用程序我发现两种方式都很有用。如果只需要一次从ViewModel到View的调用,那么带有事件处理程序的第二个选项看起来更简单,更好。但是如果你在这些层之间需要更复杂的接口,那么引入接口是有意义的。
我个人的偏好是恢复您的选项,并使用我的ViewModel实现的IViewAware接口,并将此ViewModel注入View。看起来像一个选项三。
public interface IViewAware
{
void ViewActivated();
void ViewDeactivated();
event Action CloseView;
}
public class TaskViewModel : ViewModelBase, IViewAware
{
private void FireCloseRequest()
{
var handler = CloseView;
if (handler != null)
handler();
}
#region Implementation of IViewAware
public void ViewActivated()
{
// Do something
}
public void ViewDeactivated()
{
// Do something
}
public event Action CloseView;
#endregion
}
这是一个简化的视图代码:
public View(IViewAware viewModel) : this()
{
_viewModel = viewModel;
DataContext = viewModel;
Loaded += ViewLoaded;
}
void ViewLoaded(object sender, RoutedEventArgs e)
{
Activated += (o, v) => _viewModel.ViewActivated();
Deactivated += (o, v) => _viewModel.ViewDeactivated();
_viewModel.CloseView += Close;
}
在实际应用中,我通常使用外部逻辑来连接V和VM,例如Attached Behaviors。
答案 2 :(得分:2)
我试图避免让ViewModel成为对View的引用。
在这种情况下执行此操作的方法:
从TextBox派生并添加一个依赖属性,该属性通过订阅OnSelectionChanged事件来包装CaretIndex,该事件可以让您知道插入符已移动。
通过这种方式,ViewModel能够通过绑定来了解插入符的位置。
答案 3 :(得分:1)
在我看来,第一种选择更可取。它仍然保持View和ViewModel之间的分离(通过视图界面),并将事物保存在逻辑位置。事件的使用不太直观。
我赞成在无法通过绑定实现的情况下实际使用代码,或者需要添加数百行XAML来实现我们可以通过3行代码实现的功能。
我的直觉是,如果您可以或多或少地通过代码审查背后的代码来确定正确性(这与我们对XAML的操作相同)并保持主要复杂性,我们可以对其进行单元测试 - 即ViewModel,那么我们有一个快乐的媒介。创建技术纯粹的MVVM非常容易,这是一个可维护性的噩梦。
所有恕我直言:D
答案 4 :(得分:1)
我会尝试将其实现为文本框的混合行为,类似于选择和展开树视图而不使用后面的代码的示例。我会一起尝试和举一个例子。 http://www.codeproject.com/KB/silverlight/ViewModelTree.aspx
编辑:Elad已经提到过使用附加行为,在做了几个之后,真的做了这样简单的事情。
mvvm方式弹出窗口行为的另一个示例:http://www.codeproject.com/KB/silverlight/HisowaSimplePopUpBehavior.aspx
答案 5 :(得分:1)
当控件难以与MVVM兼容时,通常需要使用代码后面的控件。在这种情况下,您可以使用AttachedProperties,EventTriggers,来自混合SDK的行为来扩展控件的功能。但是我经常使用继承来扩展控制功能并使其更兼容MVVM。您可以使用已实现的视图功能创建自己继承的控件集。这种方法的一大优点是您可以访问ControlTemplate控件,它通常需要实现特定的视图功能。