Skip to content
Antony Male edited this page Nov 28, 2014 · 1 revision

So, it's time to start thinking about how we're going to compose our Views and ViewModels.

The basic structure is shown below.

At the top-most level, we have the ShellView. This owns the TaskbarView, which is always shown, and allows the user to enter the subreddit they want to browse. The ShellView also owns a TabControl, which displays many SubredditViews, once for each subreddit which was opened.

It turns out that each SubredditView also owns a couple of other views, but we'll come to that in due course.

In this section, we're going to build the TaskbarView.

Create a new UserControl called TaskbarView.xaml, and a new ViewModel called TaskbarViewModel.cs. Note that TaskbarView.xaml has to be a UserControl, since it's displayed inside another view, not in its own window.

The contents of TaskbarView.xaml are pretty simple, and are shown below:

<UserControl x:Class="Stylet.Samples.RedditBrowser.Pages.TaskbarView"
             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"
             xmlns:s="https://github.com/canton7/Stylet"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <DockPanel LastChildFill="False">
        <DockPanel DockPanel.Dock="Top">
            <Label DockPanel.Dock="Left">Subreddit: /r/</Label>
            <Button DockPanel.Dock="Right" Command="{s:Action Open}" IsDefault="True">Open</Button>
            <ComboBox DockPanel.Dock="Right" ItemsSource="{Binding SortModes}" DisplayMemberPath="Name" SelectedItem="{Binding SelectedSortMode}"/>
            <TextBox Text="{Binding Subreddit, UpdateSourceTrigger=PropertyChanged}"/>
        </DockPanel>
    </DockPanel>
</UserControl>

The contents of TaskbarViewModel.cs are similarly simple. Don't worry about the structure of the SortMode class for now - it's just a container specifying whether the posts are sorted by new, host, etc. (Look in RedditClient.cs) if you're interested.

public class TaskbarViewModel : Screen
{
    public string Subreddit { get; set; }

    public IEnumerable<SortMode> SortModes { get; private set; }
    public SortMode SelectedSortMode { get; set; }

    public TaskbarViewModel()
    {
        this.SortModes = SortMode.AllModes;
        this.SelectedSortMode = SortMode.Hot;
    }

    public bool CanOpen
    {
       get { return !String.IsNullOrWhiteSpace(this.Subreddit); }
    }
    public void Open()
    {
        // TODO
    }
}

Notice how we're not explicitly raising an PropertyChanged events - PropertyChanged.Fody is raising those for us. Also don't be worried by the fact that SortModes is an IEnumerable<SortMode> and not a BindableCollection<SortMode> - the collection won't be modified after it's been set, so it's OK to take this shortcut here.

Also note the method guard CanOpen, which stops the user clicking "Open" when they haven't entered any text. PropertyChanged.Fody is smart enough to notice that it depends on the Subreddit property, and will raise a PropertyChanged notification for CanOpen every time that Subreddit changes.

We're also not yet handling the Open method - we'll come back to that in a bit.

Now we're written this View and ViewModel, it's time to stitch them into the ShellView and ShellViewModel.

Since ShellViewModel always owns an instance of TaskbarViewModel, we can inject the TaskbarViewModel into the ShellViewModel using the ShellViewModel's constructor, like this:

public class ShellViewModel : Screen
{
   public TaskbarViewModel Taskbar { get; private set; }
   
   public ShellViewModel(TaskbarViewModel taskbarViewModel)
   {
      this.DisplayName = "Reddit Browser";

      this.Taskbar = taskbarViewModel;
   }
}

That way, when the ShellViewModel instance is created by StyletIoC, StyletIoC will also create an instance and TaskbarViewModel and pass it into ShellViewModel's constructor. It also means that, were you to write unit tests for ShellViewModel, you could pass a mock or stub in here instead.

So, the ShellViewModel now owns a TaskbarViewModel instance. Let's make sure that ShellView can display the corresponding TaskbarView. Open up ShellViewModel.xaml, and make it look something like this:

<Window x:Class="Stylet.Samples.RedditBrowser.Pages.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:s="https://github.com/canton7/Stylet"
        Title="ShellView" Height="500" Width="500">
    <DockPanel>
        <GroupBox DockPanel.Dock="Top" Header="Navigate to">
            <ContentControl s:View.Model="{Binding Taskbar}"/>
        </GroupBox>
    </DockPanel>
</Window>

Aside from the minor point of switching to using a DockPanel, the important aspect is that ContentControl. We're bound its s:View.Model attached property to the Taskbar property on the ShellViewModel (which is an instance of TaskbarViewModel, of course). Stylet will then create a new instance of TaskbarView, bind it to the TaskbarViewModel instance, and set it as that ContentControl's Content property.

Build and run your application, and you should see the taskbar displayed at the top. Woo!

The only missing bit to the puzzle is how to handle that Open method on the TaskbarViewModel. We could do this in many ways, but I'm going to use The EventAggregator. You should really read its wiki page, but the basic idea is that our ShellViewModel will register itself as a subscriber, and the TaskbarViewModel will publish events to to the EventAggregator, which will then be delivered to the ShellViewModel.

Firstly, we need our event. Create a new class called OpenSubredditEvent, which looks like this:

public class OpenSubredditEvent
{
    public string Subreddit { get; set; }
    public SortMode SortMode { get; set; }
}

That done, we'll make the ShellViewModel implement IHandle<OpenSubredditEvent>. We'll also inject an instance of IEventAggregator, and subscribe to it, like this:

public class ShellViewModel : Screen, IHandle<OpenSubredditEvent>
{
   public TaskbarViewModel Taskbar { get; private set; }
   
   public ShellViewModel(IEventAggregator events, TaskbarViewModel taskbarViewModel)
   {
      this.DisplayName = "Reddit Browser";

      this.Taskbar = taskbarViewModel;

      events.Subscribe(this);
   }

   public void Handle(OpenSubredditEvent message)
   {
      // TODO
   }
}

We'll also need to teach the TaskbarViewModel how to publish events:

public class TaskbarViewModel : Screen
{
    private IEventAggregator events;

    public string Subreddit { get; set; }

    public IEnumerable<SortMode> SortModes { get; private set; }
    public SortMode SelectedSortMode { get; set; }

    public TaskbarViewModel(IEventAggregator events)
    {
        this.events = events;
        this.SortModes = SortMode.AllModes;
        this.SelectedSortMode = SortMode.Hot;
    }

    public bool CanOpen
    {
        get { return !String.IsNullOrWhiteSpace(this.Subreddit); }
    }
    public void Open()
    {
        this.events.Publish(new OpenSubredditEvent() { Subreddit = this.Subreddit, SortMode = this.SelectedSortMode });
    }
}

Now, whenever we click the "Open" button, the Handle(OpenSubredditEvent message) method on the ShellViewModel will be called. We'll actually make this method do something in a future section.