我应该编写如下的单元测试吗?
public ObservableCollection<DXTabItem> Tabs { get; private set; }
public ICommand CustomersCommand { get; private set; }
CustomersCommand = new DelegateCommand(OpenCustomers);
private void OpenCustomers()
{
var projectService = new ProjectService(Project.FilePath);
var vm = new CustomersViewModel(projectService);
AddTab("Customers", new CustomersView(vm));
}
public void AddTab(string tabName, object content, bool allowHide = true)
{
Tabs.Add(new DXTabItem { Header = tabName, Content = content });
}
[TestMethod]
public void CustomerCommandAddsTab()
{
_vm.CustomersCommand.Execute(null);
Assert.AreEqual("Customers", _vm.Tabs[1].Header);
}
<dx:DXTabControl ItemsSource="{Binding Tabs}" />
我正在使用TDD方法,所以这是工作代码,并且它在本地传递测试,但是在服务器CI构建上它未通过此测试,因为视图(CustomersView
)内部有一些东西没有工作。所以我意识到了这个测试,尽管它的简单实际上是在打破MVVM
。我通过引用ViewModel
在DXTabItems
内部编写UI代码,甚至新建了View
。
这样的事情的正确方法是什么?我是不是应该编写这样的测试(并且依赖于自动化测试),或者我应该以某种方式重构它以使ViewModel
不包含UI元素,关于我应该如何做的提示将是有用的。
这种设计的原因是每个选项卡包含一个不同的视图,例如Customers选项卡包含CustomersView,而另一个选项卡将包含完全不同的数据和表示。因此很难定义一种机制,允许以MVVM方式实现这一点。至少答案并非微不足道。
答案 0 :(得分:2)
如果DXTabItem是从TabItem派生的,那么这不是MVVM,在MVVM中,您永远不会直接在视图模型中访问视图元素。您应该做的是为您的选项卡创建一个视图模型(例如TabViewModel
),将选项卡更改为ObservableCollection<TabViewModel>
并将选项卡控件的ItemsSource
属性绑定到该属性以创建GUI标签本身。
对于CI失败,您不应该在单元测试中创建GUI元素(即CustomersView)。你唯一一次这样做是在集成测试期间,这是一个不同的鱼。视图应该只通过数据绑定机制松散地耦合到视图模型,您应该能够运行和测试整个应用程序而无需创建单个视图对象。
更新:实际上这很容易......一旦你知道如何! :)有几种不同的方法可以实现您的目标,但最常见的两种方法是数据模板和触发器。
使用数据模板,您依赖于视图模型应该代表GUI背后的逻辑这一事实。如果您有一个客户端选项卡和一个产品选项卡(比如说),则那些应该具有相应的视图模型,即ClientPage和ProductPage。您可能希望为这些创建基类(例如TabViewModel
),在这种情况下,您的视图模型集合将是ObservableCollection<TabViewModel>
,如上所述,否则只需将其设为ObservableCollection<object>
。然后,您可以使用数据模板指定要为每个选项卡创建的视图:
<DataTemplate DataType="{x:Type vm:ClientPage>
<view:ClientView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:ProductPage>
<view:ProductView />
</DataTemplate>
ListBox和其他集合元素将自动应用这些数据模板,或者您可以显式指定ListBox.ItemTemplate并在需要时使用ContentControl。
第二种方法是使用数据触发器。如果您的页面已修复,那么我发现在视图模型层中创建枚举会有所帮助,原因我会在一分钟内解释:
public enum PageType : int
{
Client,
Product,
... etc ...
}
回到你的XAML,你会想要为每一个创建一个页面,你可以在你的VM中做到这一点,如果你喜欢虽然这是一个很容易的任务我通常在XAML中这样做:
<ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="PageType">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="vm:PageType" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
现在您可以创建一个TabControl并将ItemsSource绑定到此对象,并为枚举中的每个项目显示一个单独的选项卡:
<TabControl ItemsSource="{Binding Source={StaticResource PageType}}"
SelectedIndex="{Binding CurrentPage, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}"
IsSynchronizedWithCurrentItem="True">
CurrentPage当然是MainTypeModel中PageType
类型的属性 private PageType _CurrentPage;
public PageType CurrentPage
{
get { return _CurrentPage; }
set { _CurrentPage = value; RaisePropertyChanged(); }
}
XAML不够智能处理枚举,所以你还需要EnumToIntConverter的代码,它可以在两者之间进行转换:
public class EnumToIntConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (int)value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return Enum.ToObject(targetType, value);
}
#endregion
}
使用这样的枚举可能看起来比需要的工作多一些,但它确实意味着您的视图模型代码现在可以通过执行类似`this.CurrentPage = PageType.Client'的任何时候设置活动页面。这在应用程序的后期阶段尤其方便,您可能希望在应用程序的其他位置有一个产品列表,并且您希望为用户提供一个打开产品页面的按钮(例如)。这为您的整个应用程序提供了对标签行为的大量控制。当然,这也意味着每当用户更改Tabs时(即当this.CurrentPage更改值时)都会收到通知,这对于按需加载数据非常有用,可以提高应用程序的性能...如果您更改了稍后您的枚举中的页面顺序,因为您的视图模型代码正在检查枚举而不是整数页码!
我没有展示的另一件事是如何在每个页面上显示相应的子内容,就像我说的那样,这是通过列表框项目样式中的数据触发器完成的:
<TabControl.Resources>
<Style TargetType="{x:Type TabItem}" BasedOn="{StaticResource {x:Type TabItem}}">
<Style.Triggers>
<!-- Client -->
<DataTrigger Binding="{Binding}" Value="{x:Static vm:PageType.Client}">
<Setter Property="Header" Value="Client" />
<Setter Property="Content">
<Setter.Value>
<view:ClientView DataContext="{Binding ElementName=parentTab, Path=DataContext.ClientPage"/>
</Setter.Value>
</Setter>
</DataTrigger>
<!-- Product -->
<DataTrigger Binding="{Binding}" Value="{x:Static vm:PageType.Product}">
<Setter Property="Header" Value="Product" />
<Setter Property="Content">
<Setter.Value>
<view:ProductView DataContext="{Binding ElementName=parentTab, Path=DataContext.ProductPage"/>
</Setter.Value>
</Setter>
</DataTrigger>
正如您所看到的,每个DataTrigger只是检查它的DataContext被设置为哪个枚举,并相应地设置它自己的标题和内容。