Reputation: 5488
Should I be writing unit tests like the following?
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}" />
I am using the TDD approach, so this is working code, and it passes the tests locally, however on a server CI build it fails this test because the view (CustomersView
) has something inside it that doesn't work. So I realized this test, even though its simple is actually breaking MVVM
. I am writing UI code inside the ViewModel
by referencing DXTabItems
and even new'ing up a View
.
What is the correct approach for something like this? Should I not write tests like this at all (and rely on automated testing) or should I refactor it somehow so that the ViewModel
contains no UI elements, tips on how I should do that would be useful.
The reason for this kind of design is that each Tab contains a different View, for example the Customers Tab contains the CustomersView, yet another Tab would contain something completely different, in data and presentation. So its hard to define a mechanism that will allow for that in MVVM fashion. At least the answer is not trivial.
Upvotes: 0
Views: 1276
Reputation: 16119
If DXTabItem is derived from TabItem then this is not MVVM, in MVVM you never access view elements directly in the view model. What you should be doing instead is creating a view model for your tabs (e.g. TabViewModel
), change Tabs to be an ObservableCollection<TabViewModel>
and bind your tab control's ItemsSource
property to that to create the GUI tabs themselves.
As for your CI failing, you shouldn't ever be creating GUI elements (i.e. CustomersView) in unit tests. The only time you'd do that is during integration testing, which is a different kettle of fish. Views should only ever be loosely coupled to the view model though the mechanism of data-binding, you should be able to both run and test your entire application without creating a single view object.
UPDATE: Actually it's very easy...once you know how! :) There are a couple of different ways to achieve what you're trying to do but the two most common approaches are data templates and triggers.
With data templates you rely on the fact that your view models are supposed to represent the logic behind your GUI. If you have a Client tab and an Product tab (say) then those should have corresponding view models i.e. ClientPage and ProductPage. You may wish to create a base class for these (e.g. TabViewModel
) in which case your view model collection would be a ObservableCollection<TabViewModel>
as I explained above, otherwise just make it a ObservableCollection<object>
. Either way you then use data templates to specify which view to create for each tab:
<DataTemplate DataType="{x:Type vm:ClientPage>
<view:ClientView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:ProductPage>
<view:ProductView />
</DataTemplate>
ListBox and other collection elements will apply these data templates automatically, alternatively you can specify ListBox.ItemTemplate explicitly and use a ContentControl where needed.
The second method is to use data triggers. If your pages are fixed then I find it helps to create an enumeration in your view model layer for reasons I'll explain in a minute:
public enum PageType : int
{
Client,
Product,
... etc ...
}
Back in your XAML you'll want to create a page for each of these, you can do that in your VM if you like although it's such an easy task I usually do it in XAML:
<ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="PageType">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="vm:PageType" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
Now you can create a TabControl and bind ItemsSource to this object and a separate tab will appear for each item in your enum:
<TabControl ItemsSource="{Binding Source={StaticResource PageType}}"
SelectedIndex="{Binding CurrentPage, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}"
IsSynchronizedWithCurrentItem="True">
CurrentPage is, of course, a property in your MainViewModel of type PageType
private PageType _CurrentPage;
public PageType CurrentPage
{
get { return _CurrentPage; }
set { _CurrentPage = value; RaisePropertyChanged(); }
}
XAML isn't smart enough to deal with enums so you'll also need the code for EnumToIntConverter which converts between the two:
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
}
Using an enum like this might seem like a bit more work than needed but it does mean that your view model code can now set the active page at any time by doing something like `this.CurrentPage = PageType.Client'. This is especially handy in the later stages of an application where you might want have a list of products somewhere else in your application and you want to provide the user with a button (say) which opens up the product page. This provides your entire application with a lot of control over the behaviour of your tabs. Of course it also means you get notification whenever the user changes Tabs (i.e. when this.CurrentPage changes value) which can be useful for loading data on demand to improve the performance of your application...it doesn't matter if you change the order of the pages around in your enum later because your view model code is checking against an enum instead of an integer page number!
The only other thing I haven't shown is how to display the appropriate child content on each of the pages, and like I said this is done with a data trigger in your listbox item style:
<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>
As you can see each DataTrigger is simply checking to see which enum its DataContext has been set to and setting it's own Header and Content accordingly.
Upvotes: 2