Vorige keer begon ik een reeks artikelen over MVVM. In dit tweede deel ga ik verder met het voorbeeld dat ik daar begonnen ben en ga ik de categorieen uitbreiden.
Specificaties
De klant wil graag voor iedere categorie van een contact een kleurtje zien. Net als in Outlook dus. Voor de eerste versie hebben we twee categorieen, namelijk Prive (geel) en zakelijk (blauw). Een contact kan 0, 1, of meerdere categorieen hebben, deze moeten bij de contact getoond worden.
Simpel dus.
Implementatie
We hebben de Category class al gedefinieerd. Laten we de ViewModel maken. Ook deze implementeert INotifyPropertyChanged, net zoals de ContactViewModel, dus het is handig om een baseclass te maken en ContactViewModel en CategoryViewModel hiervan af te leiden:
using System;
using System.ComponentModel;
namespace dotNedContactManager.ViewModels
{
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
var eventCopy = PropertyChanged;
if (eventCopy == null)
return;
eventCopy( this, new PropertyChangedEventArgs( propertyName ) );
}
}
}
We moeten nu wel even ContactViewModel aanpassen: het event eruit, de RaisePropertyChanged eruit, en niet meer INotifyPropertyChanged implementeren maar afleiden van ViewModelBase. Voor CategoryView ziet het er dan als volgt uit:
using System;
using dotNedContactManager.Model;
namespace dotNedContactManager.ViewModels
{
public class CategoryViewModel : ViewModelBase
{
private Category _Category;
public CategoryViewModel(Category category)
{
if (category == null)
throw new ArgumentNullException( "category", "category is null." );
_Category = category;
}
public string Description
{
get { return _Category.Description; }
set
{
_Category.Description = value;
RaisePropertyChanged( "Description" );
}
}
// Deze hebben we nodig in de ContactViewModel.
internal Category Category
{
get { return _Category; }
}
}
}
In ContactViewModel voegen we nu de Categories toe (die hadden we nog niet).
private ObservableCollection<CategoryViewModel> _Categories;
public ObservableCollection<CategoryViewModel> Categories
{
get
{
return _Categories;
}
}
De Categories is een ObservableCollection<CategoryViewModel>, wat inhoudt dat als er een wijziging op de collectie optreedt we daar iets mee kunnen doen. In de constructor van onze ViewModel maken we de Categories aan en zetten de events. We moeten de constructor dus aanpassen:
public ContactViewModel(Contact contact)
{
if (contact == null)
throw new ArgumentNullException( "contact", "contact is null." );
_Contact = contact;
_Categories = new ObservableCollection<CategoryViewModel>();
foreach (var category in contact.Categories)
{
_Categories.Add( new CategoryViewModel( category ) );
}
_Categories.CollectionChanged += Categories_CollectionChanged;
}
We zetten de eventhandler pas na het vullen van de initiele collectie, anders gebeuren er rare dingen. En deze hebben we ook nodig: de implementatie van de Categories_CollectionChanged:
private void Categories_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Wijzig de categorieeen in de Contact
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (CategoryViewModel newCategory in e.NewItems)
_Contact.Categories.Add( newCategory.Category );
break;
case NotifyCollectionChangedAction.Remove:
foreach (CategoryViewModel oldCategory in e.OldItems)
_Contact.Categories.Remove( oldCategory.Category );
break;
default:
// etc...
break;
}
}
Zo gauw er in de ViewModel iets verandert met de Categories van de Contact, sturen we dit direct door naar onze domeinclass Contact. Je hoeft dit niet te doen, maar voor nu is dat wel zo handig.
We gaan nu een tweetal views maken. Een voor de Category maar ook een voor de CategoryCollection. Die laatste is niets anders dan een lijst met CategoryViews maar is wel handig. Zie:
<UserControl x:Class="dotNedContactManager.Views.CategoryView"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<TextBlock Text="{Binding Description}"/>
</Grid>
</UserControl>
En de lijst:
<UserControl x:Class="dotNedContactManager.Views.CategoryListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:dotNedContactManager.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<ListBox ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<views:CategoryView DataContext="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
Nu alleen de categorieen toevoegen in onze ContactView. We hebben een namespace nodig:
xmlns:views="clr-namespace:dotNedContactManager.Views"
En de view zelf:
<views:CategoryListView Grid.Row="3" Grid.Column="1" DataContext="{Binding Categories}"/>
Ik hoop dat je al die Bindings nog kunt volgen. Even voor de duidelijkheid:
In MainWindow maken we een instance van ContactView. Deze krijgt de DataContext van de ContactViewModel. In de ContactView staan bindings naar FirstName, LastName, FullName en Categories. Dit zijn allen properties van ContactViewModel, dus die worden gevonden. Echter, de CategoryListView krijgt een binding op de datacontext ({Binding Categories}). Dus in de CategoryListView hebben we nu een DataContext (te weten: een ObservableCollection<CategoryView>). Dit wordt gebruikt als bron voor de ListBox.ItemsSource. We geven bij Binding daar geen pad op, dus hij krijgt het gehele object, i.e. de ObservableCollection. Vervolgens wordt er voor ieder item in die collection een CategoryView aangemaakt, deze krijgt als DataContext dit item mee (dus een CategoryViewModel uit de ObservableCollection). In de CategoryView hebben we nu dus een DataContext, wat in dit geval een instance van CategoryViewModel is, en die heeft de property Description. Die wordt getoond in een textbox.
Je ziet dat alles aan elkaar gelinkt wordt door bindings en niets in de code.
Voor je runt moet je uiteraard nog wel even de juiste data meegeven in MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public ContactViewModel ContactVM { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
// Tijdelijk wat data genereren
Model.Contact newContact = new Model.Contact()
{
FirstName = "Dennis",
LastName = "Vroegop"
};
Model.Category catPrivate = new Model.Category() { Description = "Prive" };
Model.Category catBusiness = new Model.Category() { Description = "Zakelijk" };
newContact.Categories.Add( catPrivate );
newContact.Categories.Add( catBusiness );
ContactVM = new ContactViewModel( newContact );
}
}
We maken even snel twee categorieen aan en geven deze mee aan ons Contact.
Run en zie:
Leuk, niet waar? Is alleen nog niet conform specificaties. We willen geen tekstuele beschrijving van de categorie, maar een vierkantje met een kleur..
Hoe gaan we dat nu doen? Deze specificatie is alleen van belang bij de User Interface. Het domein verandert niet, en eigenlijk ook de ViewModel niet. Alleen de UI moet wat anders laten zien. En dit is nu precies de kracht van MVVM: dit soort designdingen kun je overlaten aan diegene die bezig is met de UI: de onderliggende lagen veranderen niet.
We willen dus een andere view voor Category, dus CategoryView moet anders. Geen probleem. We maken een andere XAML, maar we voegen wel iets toe. Om precies te zijn: we maken een converter. Een converter is een class die wordt gebruikt voor het converteren van een bepaald type naar een ander type in Bindings. Bijvoorbeeld: het converteren van een string met een bepaalde waarde naar een kleur.. Eerst de converter schrijven:
using System;
using System.Windows.Data;
using System.Windows.Media;
namespace dotNedContactManager.Converters
{
public class DescriptionToBrushConverter : IValueConverter
{
public object Convert(object value,
Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
if (!(value is String))
return value;
var cleanedString = ((string)value).Trim().ToUpper();
if (String.IsNullOrEmpty( cleanedString ))
return Brushes.White;
switch (cleanedString)
{
case "PRIVE":
return Brushes.Yellow;
case "ZAKELIJK":
return Brushes.Blue;
default:
return Brushes.HotPink;
}
}
public object ConvertBack(object value,
Type targetType,
object parameter,
System.Globalization.CultureInfo culture)
{
// Negeren we voor nu, we gaan
// geen brushes terugconverteren naar strings
return value;
}
}
}
We krijgen een string binnen, we kijken naar de inhoud en geven een SolidColorBrush met een bepaalde kleur terug. Als we geen string krijgen, krijgen we een witte brush, als we een andere categorie krijgen dan we verwachten dan wordt hij mooi roze (zodat hij goed opvalt en we weten dat we een bug ergens hebben).
We passen de CategoryView even aan:
<UserControl x:Class="dotNedContactManager.Views.CategoryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:dotNedContactManager.Converters"
Width="64" Height="64">
<UserControl.Resources>
<converters:DescriptionToBrushConverter x:Key="descToBrushConv"/>
</UserControl.Resources>
<Grid>
<Border
BorderBrush="#8D8D8D"
BorderThickness="2"
CornerRadius="2"
Background="{Binding Description, Converter={StaticResource descToBrushConv}}">
<TextBlock Text="{Binding Description}"
Foreground="Black"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</Grid>
</UserControl>
Ik heb een namespace aangemaakt die verwijst naar de converters. In de Resources heb ik nu die converter aangemaakt en een key gegeven. Om de originele Textblock die we al hadden is nu een Border geplaatst.
De border heeft een BackGround en die wordt gebind aan Description. Uiteraard is Description een string en geen Brush, maar daar zorgt onze converter voor. Die maakt een mooie brush aan naar gelang de waarde van de description.
Runnen maar:
Stukken beter, niet waar? Een echte designer kan hier nu mee aan de gang en zich helemaal uitleven op dit soort schermen. Alle logica is immers los van de frontend. En wat de designer ook doet, zolang de Bindings er maar blijven staan werkt het allemaal wel. Wij als ontwikkelaars kunnen verder gaan met het schrijven van code voor ViewModels (en dus ook de unittests die daarbij horen).
Volgende keer meer!
Technorati Tags:
MVVM,
WPF,
Silverlight