How to Build, Manage and Navigate the User Interface of a WPF Application

How to Build, Manage and Navigate the User Interface of a WPF Application

Introduction


This article is an attempt to cover some of the most common methods of creating, managing and navigating between pages & controls of a WPF application.

 

 


The three main "stages" that we work with in WPF applications are WindowsPages and UserControls.

We will explore the various ways of navigating between views and loading forms or controls into the main view.

 

Download


An accompanying project, with working examples of all the techniques discussed here, is available at TechNet Samples

http://code.msdn.microsoft.com/How-to-Build-Manage-and-fdd0074a


Navigate Pages or Dashboard


There are two main styles to user interface development in WPF, and it depends on how you lay out your application. You could also mix the two.
  1. Using the built in navigation system to jump between individual pages of the application
  2. Use a dashboard style "main window" and just update individual portions of the screen.

NavigationWindow

The first, most fully featured and obvious controls for navigation are Frame and Page. You could include a Frame in your main window and run it all from that, but if your project is going to be completely based around Page navigation, you can simply use a NavigationWindow (from System.Windows.Navigation) instead of a Window for your main startup interface.

MainWindow.xaml

<NavigationWindow x:Class="ApplicationNavigation.MainWindow"
        Title="Navigation Window" Height="600" Width="550" Background="#FFFFE9E9" Source="View/Page0.xaml" >
</NavigationWindow>

 
This first navigation is triggered by the Source property of the NavigationWindow.

1. Hyperlink

This is the most simple method for navigating between pages within a NavigationWindow or frame. There is no code-behind needed. It is therefore quite similar to a web page hyperlink.

Page0.xaml

<TextBlock Grid.Row="1" HorizontalAlignment="Center" FontWeight="Bold" FontSize="18" Margin="0,20,0,0" >
    <Hyperlink NavigateUri="Page1.xaml">1. Click this hyperlink</Hyperlink>
</TextBlock>

 

2. Frame


Rather than base your application around a NavigationWindow, it is more common to design your own top level framework (like an HTML FrameSet) with top level menus, navigation buttons, and other application level sections. A WPF Frame is not like an HTML Frame. It is more akin to an InnerFrame. Frames also manage navigation history, and can be used the same as a NavigationWindow.

Page1.xaml
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition />
    </Grid.RowDefinitions>
    <TextBlock Text="Page 1"/>
    <Button Grid.Row="1" Click="Button_Click" Content="2. Frame Navigate" Width="200"/>
    <Frame x:Name="MyFrame" Grid.Row="2" BorderThickness="2" Margin="10,10,10,101" BorderBrush="#FFB4B4B4" Content="Frame" Padding="10"/>
</Grid>


Page1.xaml.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
    MyFrame.Navigate(new Page2());
}

In the second example, we simply call the Frame's Navigate method to load Page1

3. NavigationService


When you are using a Page within a NavigationWindow or Frame, you can navigate using the built in NavigationService. If you have nested navigation componants, like i have shown in the sample project, then each level has it's own NavigationService.

Page2.xaml.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
    this.NavigationService.Navigate(new Page3());
}

The code above uses the page's own NavigationService. The page's NavigationService uses MyFrame, not the main parent NavigationWindow's built-in frame.

Dashboard Applications


The built in navigation framework is nice, and great for simple database management applications. However, the power of WPF means so much more is possible.
 
By working with and updating "areas" of the screen, rather than the whole page, an application can come alive with effects and animations. 

Instead of Frames and Pages, the application consists of a main window (dashboard/stage), UserControls within that window, and various popup dialog windows.

Instead of navigating between pages, controls are added into "containers", which can either hold multiple Children (eg Grid, or just one logical Child

Side-note about the power of templates


With WPF, controls can even be created on the fly through binding collections of a class to the ItemsSource of controls like ListBox, DataGridItemsControl, etc.
ItemsControls use ItemTemplates & DataTemplates to define the "blue-print" of the control.

This is a subject for another article, this one is simply to discuss direct manual manipulation methods, not automated control creation.

4. Adding UserControls to XAML


When you create a UserControl, either in your main project, or a separate library project, you need to firstly add the namespace for the control, so that you can reference it in XAML. The code below shows the Markup method of including a UserControl which was created in the same project.

Page3.xaml
<Page x:Class="ApplicationNavigation.View.Page3"
      Background="#FFEBFFD5" Title="Page3"
      xmlns:views="clr-namespace:ApplicationNavigation.View">
 
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock Text="Page 3"/>
        <Border x:Name="MyStage" Grid.Row="2" BorderThickness="2" CornerRadius="10" BorderBrush="Black" Margin="10" Padding="10">
            <views:UserControl1 />
        </Border>
    </Grid>
</Page>



Initializing a view with controls (as above) is a good start, but we often need to load controls into the view on the fly, from code.
The rest of the examples show various ways to do that.


5. Parent Cast


If I complete a form and click it's submit button, then after the form is saved, I may want to navigate to the summary page.

One of the most immediate and tightly-coupled methods of navigating away, based on events within a control, is for the control itself to load the next page into it's parent container, replacing itself with the next page.

UserControl1.xaml.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
    var parent = Parent as Border;
    if (parent != null)
        parent.Child = new UserControl2(parent);
}

A problem with this approach is if we change the design of the parent page (for example changing the Border to a GroupBox) it will compile, but we get a null from the cast, so no navigation.


6. Inversion of Control / Dependancy Injection (kind of)


One way to ensure compile-time checks protect your method is to pass the parent in as a constructor parameter. In this example, it is defined as a Border.

IoC and DI are based around this concept of passing the controller into the player. This is usually done with an interface, so all players know what to expect, no matter who the controller that uses them is.

UserControl2.xaml.cs
using System.Windows;
using System.Windows.Controls;
 
namespace ApplicationNavigation.View
{
    public partial class UserControl2 : UserControl
    {
        Border _parentBorder;
        public UserControl2(Border parentBorder)
        {
            _parentBorder = parentBorder;
            InitializeComponent();
        }

This technique actually comes in useful a lot in a project's development, throughout most child UserControls and popup dialog windows.

A better variation of this would be to pass in a delegate method like a "ReloadUser" method, to tell the parent that data is saved, and needs to be repulled from the database.

In this example, we set it's parent (Border) Child, thereby destroying itself.

UserControl2.xaml.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
    if (_parentBorder != null)
        _parentBorder.Child = new UserControl3();
}


7. Application Controller (MVC)


The two methods above both pass control directly from control to control. One side effect of this is that navigation code is buried throughout the code.

A better approach for any reasonable sized project is to centralize all main navigation code into one place.
This is similar to the MVC concept of a Controller that creates the View from the Model.
This method works well with an MVVM based project too, navigation being a separate "application level" concern from the data anyway.

There are many ways and places to initialize such a class too. In this example we pass it in as we reach the part of the project that introduces this "stage" that will be used.

Page3.xaml.cs
public Page3()
{
    InitializeComponent();
    ApplicationController.RootBorder = MyStage;
    ...
}

An Application Controller for an otherwise MVVM based project typically has methods like ShowUserPage(int userId) which creates the View, the ViewModel and binds the ViewModel to the DataContext of the View (see a later example). In this simpler example, it just takes the next View as a parameter and loads it into the registered root control.

ApplicationController.cs
using System.Windows.Controls;
 
namespace ApplicationNavigation.Helpers
{
    public class ApplicationController
    {
        public static Border RootBorder { get; set; }
 
        public static void LoadUserControl(UserControl nextControl)
        {
            RootBorder.Child = nextControl;
        }
    }
}

This can then be called from anywhere in the project, to navigate the application to the next page.

UserControl3.xaml.cs
    ApplicationController.LoadUserControl(newControl);


8. Events


Next is a traditional way of passing control back to a parent control - through classic event handling by the parent.

In our previous example, before we pass the UserConrtrol4 into LoadUserControl, an event handler is attached to an event of the control:

UserControl3.xaml.cs (again)
private void Button_Click(object sender, RoutedEventArgs e)
{
    var newControl = new UserControl4();
    newControl.NavigateEvent += new EventHandler(newControl_NavigateEvent);
    ApplicationController.LoadUserControl(newControl);
}
 
void newControl_NavigateEvent(object sender, EventArgs e)
{
    ApplicationController.LoadUserControl(new UserControl5());
}

This is then triggered in the usual manner:

public event EventHandler NavigateEvent;
 
public UserControl4()
{
    InitializeComponent();
}
 
private void Button_Click(object sender, RoutedEventArgs e)
{
    if (NavigateEvent != null) NavigateEvent(this, null);
}

You could even pass the next page or a token back.
This method, like any event based code, can cause memory leak problems, if not disposed of properly.
So this is a last resort method of communications, for many.

9. Mediator


The Mediator pattern represents a global level of communications. You simply have to register listeners, anywhere in code, and when you fire off a signal from anywhere else in the code, the listeners all still react.

This is one of my personal favorites for passing signals around a very modular project. It allows you to pass signals from ViewModel to ViewModel, when there isn't a VisualTree to traverse. It is also very easy to start using it all the time, which leads to "spaghetti coding", so should be used sparingly.

The following example of a Mediator covers most scenarios where you would need cross-application messaging:

Mediator.cs
static IDictionary<string, List<Action<object>>> pl_dict = new Dictionary<string, List<Action<object>>>();
 
static public void Register(string token, Action<object> callback)
{
    if (!pl_dict.ContainsKey(token))
    {
        var list = new List<Action<object>>();
        list.Add(callback);
        pl_dict.Add(token, list);
    }
    else
    {
        bool found = false;
        foreach (var item in pl_dict[token])
            if (item.Method.ToString() == callback.Method.ToString())
                found = true;
        if (!found)
            pl_dict[token].Add(callback);
    }
}


This is a fairly simple, but complete example of the technique.
It is just based on a unique key AND delegate method name. However, this will cover most common scenarios.

A listener was registered back in Page3.xaml.cs.
It registers GoNextUserControl as the method delegate to trigger when a message with the token "NavigateMessage" is heard.

Page3.xaml.cs (again)
public Page3()
{
    InitializeComponent();
    ...
    Mediator.Register("NavigateMessage", GoNextUserControl);
}
 
void GoNextUserControl(object param)
{
    MyStage.Child = param as UserControl6;
}

For a better example of the Mediator, see Josh Smith's Messenger class in MVVMLite, or learn from Sacha Barber's Mediator example, which he added to Cinch.

10. Binding & INotifyPropertyChanged


WPF binding is NOT WinForm binding.

WPF binding is easy to use, incredibly flexible and reduces HUGE swathes of code that you'd normally have to write, to shuttle data to and from the UI.
There is an initial extra hit implementing INotifyPropertyChanged (INPC) in your class and properties, which puts some off initially, but the pay-off is truly immense.
You can literally write an entire database driven project, with the only code being the database layer.

Binding to a property means you can pull the initial value straight into the control, and changes will be sent back to the bound property.
However, if you change the property in code, the Ui would not normally know of this change without some kind of notification.
That is what INotifyPropertyChanged does. It fires off a PropertyChanged event.

In this example, for expediency, I am using the code-behind as the ViewModel, simply by settng the page's DataContext to itself.

UserControl6.xaml.cs (part 1)
namespace ApplicationNavigation.View
{
    public partial class UserControl6 : UserControl, INotifyPropertyChanged
    {
        UIElement _CurrentPage;
        public UIElement CurrentPage
        {
            get
            {
                return _CurrentPage;
            }
            set
            {
                if (_CurrentPage != value)
                {
                    _CurrentPage = value;
                    RaisePropertyChanged("CurrentPage");
                }
            }
        }
 
        public UserControl6()
        {
            InitializeComponent();
            DataContext = this;
        }
 
        void RaisePropertyChanged(string prop)
        {
            if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(prop)); }
        }
        public event PropertyChangedEventHandler PropertyChanged;
 
 
                         ... see part 2
    }
}

Here is a UserControl which implements INPC. For these type of properties, being in a UserControl, we could have just used a DependencyProperty on UserContrrol6, as it is a DependencyObject anyway, so it already handles such update notifications (see next example).

http://msdn.microsoft.com/en-us/library/ms752914.aspx - Dependency Properties Overview

When you create a binding, to this "CurrentPage" property, or a DependencyProperty, a WeakEventManager is automatically created for the binding, which listens for PropertyChanged events and updates the specific bindings, when it hears one.

UserControl6.xaml
    <ContentPresenter Content="{Binding CurrentPage}" Grid.RowSpan="2"/>

Now all we do is update the property from code, as would be the MVVM way, and it automatically updates the UI with the new control.

UserControl6.xaml.cs (part 2)
private void Button_Click(object sender, RoutedEventArgs e)
{
    CurrentPage = new UserControl7();
}

11. MVVM Dashboard Example


This final example is an MVVM style dashboard application, which loads user details into a UserControl, depending on the ComboBox selection.

UserControl7.xaml
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="22" />
        <RowDefinition />
    </Grid.RowDefinitions>
    <TextBlock Text="UserControl 7"/>
    <StackPanel Grid.Row="1" Orientation="Horizontal" >
        <TextBlock FontWeight="Bold" Text="10. MVVM - Please select:" Margin="0,0,10,0" VerticalAlignment="Center"/>
        <ComboBox ItemsSource="{StaticResource People}" DisplayMemberPath="FirstName" SelectedItem="{Binding SelectedPerson, Mode=TwoWay}" />     
    </StackPanel>
    <GroupBox Margin="10" Header="User Management" Content="{Binding ManagementControl}" Grid.Row="2"/>
</Grid>

This actual scenario best suits a master/detail style user interface, where the details section is bound directly to ComboBox.SelectedItem. However for the purpose of this slightly contrived example, we are using the selection to go and build the View/ViewModel, using the SelectedItem in the ViewModel DataContext.

Unlike the last basic INPC example, UserControl7 has a DependencyProperty for the UIElement to be placed.

UserControl7.xaml.cs (part 1)
public UIElement ManagementControl
{
    get { return (UIElement)GetValue(ManagementControlProperty); }
    set { SetValue(ManagementControlProperty, value); }
}
 
public static readonly DependencyProperty ManagementControlProperty =
    DependencyProperty.Register("ManagementControl", typeof(UIElement), typeof(UserControl7), new UIPropertyMetadata(null));

This notifies the same way as the INPC example, but instead of applying change logic to the property setter, we can now define a PropertyChangedCallback Delegate

UserControl7.xaml.cs (part 2)
public Person SelectedPerson
{
    get { return (Person)GetValue(SelectedPersonProperty); }
    set { SetValue(SelectedPersonProperty, value); }
}
 
public static readonly DependencyProperty SelectedPersonProperty =
    DependencyProperty.Register("SelectedPerson", typeof(Person), typeof(UserControl7), new UIPropertyMetadata(null, SelectedPersonChanged));
 
static void SelectedPersonChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
    var person = e.NewValue as Person;
    if (person != null)
    {
        var view = ApplicationController.MakePersonAdminControl(person.Id);
        (obj as UserControl7).ManagementControl = view;
    }
}

The great thing about DependancyPropertyChanged handler is that you have access to the old and new values in the args, and also the original object that is using the property (UserControl7). So we can access and use that object directly.

http://msdn.microsoft.com/en-us/library/ms745795.aspx - Dependency Property Callbacks and Validation

We have again used the ApplicationController class, to manage the task of creating View & ViewModel. This keeps UI related tasks grouped and separated into one place.

ApplicationController.cs (again)
public static UIElement MakePersonAdminControl(int id)
{
    var view = new PersonAdminControl(); // Note: No parameters, just a dumb skin
    view.DataContext = new PersonViewModel(id);
    return view;
}

As shown above, the ViewModel is created from the id parameter, and attached to the View.

In our example, the View is a TabControl, with two tabs. There are a number of control binding examples included, including ComboBox and Slider.
As an extra, extra bonus, this example also shows how to create dummy or static data in XAML

App.xaml
<x:Array x:Key="People" Type="model:Person" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <model:Person>
        <model:Person.Id>1</model:Person.Id>
        <model:Person.FirstName>Peter</model:Person.FirstName>
        <model:Person.LastName>Laker</model:Person.LastName>
        <model:Person.Age>42</model:Person.Age>
        <model:Person.DepartmentId>1</model:Person.DepartmentId>
        <model:Person.HappyRating>90</model:Person.HappyRating>
        <model:Person.StartDate>12/12/12 0:0:0</model:Person.StartDate>
    </model:Person>
    <model:Person>
        <model:Person.Id>2</model:Person.Id>
        <model:Person.FirstName>Joe</model:Person.FirstName>
        <model:Person.LastName>Bloggs</model:Person.LastName>
        <model:Person.Age>30</model:Person.Age>
        <model:Person.DepartmentId>3</model:Person.DepartmentId>
        <model:Person.HappyRating>50</model:Person.HappyRating>
        <model:Person.StartDate>11/11/11</model:Person.StartDate>
    </model:Person>
    <model:Person>
        <model:Person.Id>3</model:Person.Id>
        <model:Person.FirstName>Jane</model:Person.FirstName>
        <model:Person.LastName>Doe</model:Person.LastName>
        <model:Person.Age>24</model:Person.Age>
        <model:Person.DepartmentId>2</model:Person.DepartmentId>
        <model:Person.HappyRating>0</model:Person.HappyRating>
        <model:Person.IsDead>True</model:Person.IsDead>
    </model:Person>
</x:Array>

In a real application, this collection would have been created, exposed and bound to, in the ViewModel.
public ObservableCollection<Person> Persons { get; set; }

The xaml array shown in the project is just to introduce a way of using static data.




The project described here is available to download at MSDN Samples :

http://code.msdn.microsoft.com/How-to-Build-Manage-and-fdd0074a

Sort by: Published Date | Most Recent | Most Useful
Comments
Page 1 of 1 (1 items)