|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThis article explores how to use the ViewModel pattern to make it easier to work with the Background of TreeViewThe In Window Forms, it is very easy to use the In contrast, the WPF If you are curious to see some examples of how the WPF Background of ViewModelBack in 2005, John Gossman blogged about the Model-View-ViewModel pattern that his team at Microsoft was using to create Expression Blend (then known as ‘Sparkle’). It is quite similar to Martin Fowler’s Presentation Model pattern, only it fills in the gap between the presentation model and the view with WPF’s rich data binding. After Dan Crevier wrote his fantastic DataModel-View-ViewModel series of blog posts, the (D)MVVM pattern started growing in popularity. The (Data)Model-View-ViewModel pattern is similar to the classic Model-View-Presenter, except you have a model tailor-made for the View, called the ViewModel. The ViewModel contains all the UI-specific interfaces and properties necessary to make it easy to develop a user interface. The View binds to the ViewModel, and executes commands to request an action from it. The ViewModel, in turn, communicates with the Model, and tells it to update in response to user interaction. This makes it easier to create a user interface (UI) for the application. The easier it is to slap a UI on an application, the easier it is for a technically challenged Visual Designer to create a beautiful UI in Blend. Also, the more loosely coupled the UI is to the application functionality, the more testable that functionality becomes. Who does not want a beautiful UI and a suite of clean, effective unit tests? What Exactly Makes the TreeView so Difficult?The The fundamental problem with treating the WPF The fun does not stop there! If you want to get a As you can see, the WPF ViewModel Comes to the RescueWPF is great because it practically requires you to separate an application’s data from the UI. All of the problems listed in the previous section derive from trying to go against the grain and treat the UI as a backing store. Once you stop treating the Rather than writing code that walks up and down the items in a Now, it is time to see how to implement these concepts. The Demo SolutionThis article is accompanied by two demo applications, available for download at the top of this page. The solution has two projects. The BusinessLib class library project contains simple domain classes, used as mere data transfer objects. It also contains a Here is a screenshot of the solution’s Solution Explorer:
Demo 1 – Family Tree with Text SearchThe first demo application we will examine populates a
When the user types in some search text and presses Enter, or clicks the 'Find' button, the first matching item will display. Continuing the search will cycle through each matching item. All of that logic is in the ViewModel. Before getting too far into how the ViewModel works, let’s first examine the surrounding code. Here is the public partial class TextSearchDemoControl : UserControl
{
readonly FamilyTreeViewModel _familyTree;
public TextSearchDemoControl()
{
InitializeComponent();
// Get raw family tree data from a database.
Person rootPerson = Database.GetFamilyTree();
// Create UI-friendly wrappers around the
// raw data objects (i.e. the view-model).
_familyTree = new FamilyTreeViewModel(rootPerson);
// Let the UI bind to the view-model.
base.DataContext = _familyTree;
}
void searchTextBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
_familyTree.SearchCommand.Execute(null);
}
}
The constructor shows how we convert raw data objects into a ViewModel, and then set that as the /// <summary>
/// A simple data transfer object (DTO) that contains raw data about a person.
/// </summary>
public class Person
{
readonly List<Person> _children = new List<Person>();
public IList<Person> Children
{
get { return _children; }
}
public string Name { get; set; }
}
PersonViewModelSince the public FamilyTreeViewModel(Person rootPerson)
{
_rootPerson = new PersonViewModel(rootPerson);
_firstGeneration = new ReadOnlyCollection<PersonViewModel>(
new PersonViewModel[]
{
_rootPerson
});
_searchCommand = new SearchFamilyTreeCommand(this);
}
The private public PersonViewModel(Person person)
: this(person, null)
{
}
private PersonViewModel(Person person, PersonViewModel parent)
{
_person = person;
_parent = parent;
_children = new ReadOnlyCollection<PersonViewModel>(
(from child in _person.Children
select new PersonViewModel(child, this))
.ToList<PersonViewModel>());
}
/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is selected.
/// </summary>
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
}
}
This property has nothing to do with a “person”, but is simply a state used to synchronize the View with the ViewModel. Note that the property’s setter calls into an A more interesting example of a presentation member on /// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is expanded.
/// </summary>
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != _isExpanded)
{
_isExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (_isExpanded && _parent != null)
_parent.IsExpanded = true;
}
}
As I mentioned before, public string Name
{
get { return _person.Name; }
}
The User InterfaceThe XAML for the <TreeView ItemsSource="{Binding FirstGeneration}">
<TreeView.ItemContainerStyle>
<!--
This Style binds a TreeViewItem to a PersonViewModel.
-->
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Another piece of this demo’s UI is the search area. That area provides the user with a <StackPanel
HorizontalAlignment="Center"
Margin="4"
Orientation="Horizontal"
>
<TextBlock Text="Search for:" />
<TextBox
x:Name="searchTextBox"
KeyDown="searchTextBox_KeyDown"
Margin="6,0"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
Width="150"
/>
<Button
Command="{Binding SearchCommand}"
Content="_Find"
Padding="8,0"
/>
</StackPanel>
Now, let’s see the code in FamilyTreeViewModelThe search functionality is encapsulated in the /// <summary>
/// Gets/sets a fragment of the name to search for.
/// </summary>
public string SearchText
{
get { return _searchText; }
set
{
if (value == _searchText)
return;
_searchText = value;
_matchingPeopleEnumerator = null;
}
}
When the user clicks the 'Find' button, the /// <summary>
/// Returns the command used to execute a search in the family tree.
/// </summary>
public ICommand SearchCommand
{
get { return _searchCommand; }
}
private class SearchFamilyTreeCommand : ICommand
{
readonly FamilyTreeViewModel _familyTree;
public SearchFamilyTreeCommand(FamilyTreeViewModel familyTree)
{
_familyTree = familyTree;
}
public bool CanExecute(object parameter)
{
return true;
}
event EventHandler ICommand.CanExecuteChanged
{
// I intentionally left these empty because
// this command never raises the event, and
// not using the WeakEvent pattern here can
// cause memory leaks. WeakEvent pattern is
// not simple to implement, so why bother.
add { }
remove { }
}
public void Execute(object parameter)
{
_familyTree.PerformSearch();
}
}
If you are familiar with my WPF techniques and philosophies, you might be surprised to see that I am not using a routed command here. I normally prefer routed commands, for a multitude of reasons, but in this situation, it is cleaner and simpler to use a plain The search logic has absolutely no dependencies on IEnumerator<PersonViewModel> _matchingPeopleEnumerator;
string _searchText = String.Empty;
void PerformSearch()
{
if (_matchingPeopleEnumerator == null || !_matchingPeopleEnumerator.MoveNext())
this.VerifyMatchingPeopleEnumerator();
var person = _matchingPeopleEnumerator.Current;
if (person == null)
return;
// Ensure that this person is in view.
if (person.Parent != null)
person.Parent.IsExpanded = true;
person.IsSelected = true;
}
void VerifyMatchingPeopleEnumerator()
{
var matches = this.FindMatches(_searchText, _rootPerson);
_matchingPeopleEnumerator = matches.GetEnumerator();
if (!_matchingPeopleEnumerator.MoveNext())
{
MessageBox.Show(
"No matching names were found.",
"Try Again",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
}
IEnumerable<PersonViewModel> FindMatches(string searchText, PersonViewModel person)
{
if (person.NameContainsText(searchText))
yield return person;
foreach (PersonViewModel child in person.Children)
foreach (PersonViewModel match in this.FindMatches(searchText, child))
yield return match;
}
Demo 2 – Geographic Breakdown with Load-On-DemandThe next demo application populates a Each of the presentation classes derives from the
As I mentioned above, there are three separate data classes here, and each data class has an associated presentation class. All of those presentation classes derive from a interface ITreeViewItemViewModel : INotifyPropertyChanged
{
ObservableCollection<TreeViewItemViewModel> Children { get; }
bool HasDummyChild { get; }
bool IsExpanded { get; set; }
bool IsSelected { get; set; }
TreeViewItemViewModel Parent { get; }
}
The public partial class LoadOnDemandDemoControl : UserControl
{
public LoadOnDemandDemoControl()
{
InitializeComponent();
Region[] regions = Database.GetRegions();
CountryViewModel viewModel = new CountryViewModel(regions);
base.DataContext = viewModel;
}
}
That constructor is simply loading up some data objects from the BusinessLib assembly, creating some UI-friendly wrappers out of them, and then letting the View bind to those wrappers. The View’s /// <summary>
/// The ViewModel for the LoadOnDemand demo. This simply
/// exposes a read-only collection of regions.
/// </summary>
public class CountryViewModel
{
readonly ReadOnlyCollection<RegionViewModel> _regions;
public CountryViewModel(Region[] regions)
{
_regions = new ReadOnlyCollection<RegionViewModel>(
(from region in regions
select new RegionViewModel(region))
.ToList());
}
public ReadOnlyCollection<RegionViewModel> Regions
{
get { return _regions; }
}
}
The interesting code is in protected TreeViewItemViewModel(TreeViewItemViewModel parent, bool lazyLoadChildren)
{
_parent = parent;
_children = new ObservableCollection<TreeViewItemViewModel>();
if (lazyLoadChildren)
_children.Add(DummyChild);
}
/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is expanded.
/// </summary>
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != _isExpanded)
{
_isExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (_isExpanded && _parent != null)
_parent.IsExpanded = true;
// Lazy load the child items, if necessary.
if (this.HasDummyChild)
{
this.Children.Remove(DummyChild);
this.LoadChildren();
}
}
}
/// <summary>
/// Returns true if this object's Children have not yet been populated.
/// </summary>
public bool HasDummyChild
{
get { return this.Children.Count == 1 && this.Children[0] == DummyChild; }
}
/// <summary>
/// Invoked when the child items need to be loaded on demand.
/// Subclasses can override this to populate the Children collection.
/// </summary>
protected virtual void LoadChildren()
{
}
The actual work of loading an object’s child items is left for the subclasses to handle. They override the public class RegionViewModel : TreeViewItemViewModel
{
readonly Region _region;
public RegionViewModel(Region region)
: base(null, true)
{
_region = region;
}
public string RegionName
{
get { return _region.RegionName; }
}
protected override void LoadChildren()
{
foreach (State state in Database.GetStates(_region))
base.Children.Add(new StateViewModel(state, this));
}
}
This demo's user interface only contains a <TreeView ItemsSource="{Binding Regions}">
<TreeView.ItemContainerStyle>
<!--
This Style binds a TreeViewItem to a TreeViewItemViewModel.
-->
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate
DataType="{x:Type local:RegionViewModel}"
ItemsSource="{Binding Children}"
>
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16"
Margin="3,0" Source="Images\Region.png" />
<TextBlock Text="{Binding RegionName}" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate
DataType="{x:Type local:StateViewModel}"
ItemsSource="{Binding Children}"
>
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16"
Margin="3,0" Source="Images\State.png" />
<TextBlock Text="{Binding StateName}" />
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:CityViewModel}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16"
Margin="3,0" Source="Images\City.png" />
<TextBlock Text="{Binding CityName}" />
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
ConclusionIf you have ever battled with the WPF Special ThanksI would like to thank Sacha Barber for encouraging me to write this article. He also gave me invaluable feedback and requests while I worked on the demo applications. If it wasn’t for him, I probably would have never written this article. Revision History
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||