Building a Composite WPF and Silverlight Application with Prism – Part 3


Welcome to this four part series on building a WPF and Silverlight application with Prism. The previous parts are

Building a Composite WPF and Silverlight Application with Prism – Part 1

Building a Composite WPF and Silverlight Application with Prism – Part 2

In this third tutorial, we will create a Digg search view using the Model-View-View-Model (MVVM) pattern, create the Digg search service, and finally demonstrate how to use dependency injection to “inject” views into the View Models.

This tutorial is in both C# and Visual Basic, but when creating projects in Visual Studio, the images I may use may be C# templates for example, but you should be able to do exactly the same in Visual Basic and vica-versa. It avoids the repetition of posting two images with “Open C# Silverlight Application” and “Open Visual Basic Application”, when the Visual Studio templates are the same – bar the language. I will however, post code samples in both languages

Implementing the View using MVVM

The first thing to do is to separate the View from the View Model. We do this because

  1. View Models are far much more testable
  2. It works well with re-styling

Add another class called DiggSearchResultsViewModel. The View Model typically has all the properties that you want to bind to. Within this View Model we will have a DiggStory object, so add another class called DiggStory

Projects

In this DiggStory add the following properties

Visual Basic

Public Class DiggStory

    Public Class DiggStory

        Private _Id As Integer

        Public Property Id() As Integer

            Get

                Return _Id

            End Get

            Set(ByVal value As Integer)

                _Id = value

            End Set

        End Property

        Private _Title As String

        Public Property Title() As String

            Get

                Return _Title

            End Get

            Set(ByVal value As String)

                _Title = value

            End Set

        End Property

        Private _Description As String

        Public Property Description() As String

            Get

                Return _Description

            End Get

            Set(ByVal value As String)

                _Description = value

            End Set

        End Property

        Private _NumDiggs As Integer

        Public Property NumDiggs() As Integer

            Get

                Return _NumDiggs

            End Get

            Set(ByVal value As Integer)

                _NumDiggs = value

            End Set

        End Property

        Private _HrefLink As Uri

        Public Property HrefLink() As Uri

            Get

                Return _HrefLink

            End Get

            Set(ByVal value As Uri)

                _HrefLink = value

            End Set

        End Property

        Private _Thumbnail As String

        Public Property Thumbnail() As String

            Get

                Return _Thumbnail

            End Get

            Set(ByVal value As String)

                _Thumbnail = value

            End Set

        End Property

        Private _UserName As String

        Public Property UserName() As String

            Get

                Return _UserName

            End Get

            Set(ByVal value As String)

                _UserName = value

            End Set

        End Property

    End Class

End Class

C#

using System;

namespace NewsAggregator.Digg

{

    public class DiggStory

    {

        public int Id { get; set; }

        public string Title { get; set; }

        public string Description { get; set; }

        public int NumDiggs { get; set; }

        public Uri HrefLink { get; set; }

        public string Thumbnail { get; set; }

        public string UserName { get; set; }

    }

}

A list of these stories is now required, so in the DiggSearchResultsViewModel create the following collection, and add a dummy story in the constructor of the form (note we only need the title here). The ObvervableCollection provides a way to track changes make to the collection.

Visual Basic

Imports System.Collections.ObjectModel

Public Class DiggSearchResultsViewModel

    Public Sub New()

        Stories = New ObservableCollection(Of DiggStory)()

        Dim story As New DiggStory()

        story.Title = “I am here, Digg it”

        Stories.Add(story)

    End Sub

    Private _Stories As ObservableCollection(Of DiggStory)

    Public Property Stories() As ObservableCollection(Of DiggStory)

        Get

            Return _Stories

        End Get

        Private Set(ByVal value As ObservableCollection(Of DiggStory))

            _Stories = value

        End Set

    End Property

End Class

C#

using System.Collections.ObjectModel;

namespace NewsAggregator.Digg

{

    public class DiggSearchResultsViewModel

    {

        public DiggSearchResultsViewModel()

        {

            Stories = new ObservableCollection<DiggStory>();

            Stories.Add(new DiggStory(){Title = “I am here, Digg it”});

        }

        public ObservableCollection<DiggStory> Stories

        {

            get;

            private set;

        }

    }

}

Connecting the Model with the View Model

To connect these together we will use constructor injection again. In the DiggSearchResultsView controls constructor add the ViewModel and set the DataContext object of the control to the view model.

Visual Basic

Partial Public Class DiggSearchResultsView

    Inherits UserControl

    Public Sub New(ByVal viewModel As DiggSearchResultsViewModel)

        InitializeComponent()

        Me.DataContext = viewModel

    End Sub

End Class

C#

namespace NewsAggregator.Digg

{

    public partial class DiggSearchResultsView : UserControl

    {

        public DiggSearchResultsView(DiggSearchResultsViewModel viewModel)

        {

            InitializeComponent();

            this.DataContext = viewModel;

        }

    }

}

In the XAML for the control, delete the TextBlock with the message in it, and add a ListBox instead

<UserControl x:Class=”NewsAggregator.Digg.DiggSearchResultsView”

   xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation&#8221;

   xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”&gt;

    <Grid x:Name=”LayoutRoot” Background=”White”>

        <ListBox Name=”storiesList” ItemsSource=”{Binding Stories}”></ListBox>

    </Grid>

</UserControl>

Click F5 to run the program and you should have the following

DiggStory

Add a DataTemplate to the ListBox to format the Stories (or story in this case). You will see that there is quite a bit of XAML, but most of it is to do with styling

<UserControl x:Class=”NewsAggregator.Digg.DiggSearchResultsView”

   xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation&#8221;

   xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml&#8221; >

    <UserControl.Resources>

        <Style x:Key=”StoriesList” TargetType=”ListBox”>

            <Setter Property=”Margin” Value=”5″/>

            <Setter Property=”Grid.Row” Value=”1″/>

        </Style>

        <Style x:Key=”DiggPanel” TargetType=”StackPanel”>

            <Setter Property=”Margin” Value=”10″/>

            <Setter Property=”Width” Value=”55″/>

            <Setter Property=”Height” Value=”55″/>

            <Setter Property=”Background”>

                <Setter.Value>

                    <LinearGradientBrush EndPoint=”0.5,1″ StartPoint=”0.5,0″>

                        <GradientStop Color=”#FFFFF098″/>

                        <GradientStop Color=”#FFFFF9D4″ Offset=”1″/>

                    </LinearGradientBrush>

                </Setter.Value>

            </Setter>

        </Style>

        <Style x:Key=”NumDigsBlock” TargetType=”TextBlock”>

            <Setter Property=”HorizontalAlignment” Value=”Center”/>

            <Setter Property=”FontSize” Value=”18″/>

            <Setter Property=”FontWeight” Value=”Bold”/>

            <Setter Property=”Foreground” Value=”DarkSlateGray”/>

        </Style>

        <Style x:Key=”NumDigsSubBlock” TargetType=”TextBlock”>

            <Setter Property=”HorizontalAlignment” Value=”Center”/>

            <Setter Property=”FontSize” Value=”14″/>

            <Setter Property=”Foreground” Value=”DarkSlateGray”/>

        </Style>

        <Style x:Key=”ThumbNailPreview” TargetType=”Image”>

            <Setter Property=”Margin” Value=”7,7,5,5″/>

            <Setter Property=”Height” Value=”55″/>

        </Style>

        <Style x:Key=”TitleBlock” TargetType=”TextBlock”>

            <Setter Property=”FontSize” Value=”12″/>

            <Setter Property=”TextAlignment” Value=”Left”/>

            <Setter Property=”VerticalAlignment” Value=”Center”/>

        </Style>

    </UserControl.Resources>

    <Grid>

        <ListBox x:Name=”StoriesList” Style=”{StaticResource StoriesList}” ItemsSource=”{Binding Stories}”>

            <ListBox.ItemTemplate>

                <DataTemplate>

                    <StackPanel Orientation=”Horizontal”>

                        <!– Yellow Digg Panel with NumDiggs–>

                        <StackPanel Style=”{StaticResource DiggPanel}” >

                            <TextBlock Text=”{Binding NumDiggs}” Style=”{StaticResource NumDigsBlock}” />

                            <TextBlock Text=”diggs” Style=”{StaticResource NumDigsSubBlock}” />

                        </StackPanel>

                        <!– Story Thumbnail Preview –>

                        <Image Source=”{Binding ThumbNail}” Style=”{StaticResource ThumbNailPreview}” />

                        <!– Story Title–>

                        <TextBlock Text=”{Binding Title}” Margin=”5″ Style=”{StaticResource TitleBlock}”/>

                    </StackPanel>

                </DataTemplate>

            </ListBox.ItemTemplate>

        </ListBox>

    </Grid>

</UserControl>

If you F5 to run the program you should get

DiggClient

  Adding the Digg Service

First add an interface that will be a contract for the Digg service, call this IDiggService. This Interface will contain one method called BeginSearch that will do two things

  1. It will start the search using the query parameter
  2. It will provide a call-back mechanism for when the search is completed

This is because requests like web service calls in Silverlight must always be carried out asynchronously.

Visual Basic

Public Interface IDiggService

    Sub BeginSearch(ByVal query As String, ByVal SearchCompleteCallback As Action(Of IEnumerable(Of DiggStory)))

End Interface

C#

using System;

using System.Collections.Generic;

namespace NewsAggregator.Digg

{

    public interface IDiggService

    {

        void BeginSearch(string query, Action<IEnumerable<DiggStory>> SearchCompleteCallback);

    }

}

You can now either implement the Service or View, but I will start with the view first.

Implementing the View

In the constructor for the DiggSearchResultsViewModel add a reference to IDiggService. Here we will then perform a dummy query for “baseball”. When this query is complete it will return some stories (hopefully) pertaining to baseball. These then need to be added to a list in the OnSearchComplete method. The only thing you need to do to view the stories returned from the list is to add them to the Stories already defined.

Visual Basic

Imports System.Collections.ObjectModel

Public Class DiggSearchResultsViewModel

    Private _Stories As ObservableCollection(Of DiggStory)

    Private diggService As IDiggService

    Public Property Stories() As ObservableCollection(Of DiggStory)

        Get

            Return _Stories

        End Get

        Private Set(ByVal value As ObservableCollection(Of DiggStory))

            _Stories = value

        End Set

    End Property

    Public ReadOnly Property HeaderInfo() As String

        Get

            Return “Digg Search Results”

        End Get

    End Property

    Public Sub New(ByVal diggService As IDiggService)

        Stories = New ObservableCollection(Of DiggStory)()

        Dim dummyStory As New DiggStory()

        dummyStory.Title = “I am here, Digg it”

        Stories.Add(dummyStory)

        Me.diggService = diggService

        Me.diggService.BeginSearch(“baseball”, AddressOf OnSearchComplete)

    End Sub

    Private Sub OnSearchComplete(ByVal newStories As IEnumerable(Of DiggStory))

        Me.Stories.Clear()

        For Each diggStory In newStories

            Me.Stories.Add(diggStory)

        Next

    End Sub

End Class

C#

using System.Collections.ObjectModel;

using System.Collections.Generic;

namespace NewsAggregator.Digg

{

    public class DiggSearchResultsViewModel

    {

        private IDiggService diggService;

        public DiggSearchResultsViewModel(IDiggService diggService)

        {

            Stories = new ObservableCollection<DiggStory>();

            Stories.Add(new DiggStory(){Title = “I am here, Digg it”});

            this.diggService = diggService;

            this.diggService.BeginSearch(“baseball”, OnSearchComplete);

        }

        private void OnSearchComplete(IEnumerable<DiggStory> newStories)

        {

            this.Stories.Clear();

            foreach (var diggStory in newStories)

            {

                this.Stories.Add(diggStory);

            }

        }

        public ObservableCollection<DiggStory> Stories

        {

            get;

            private set;

        }

    }

}

Implementing the Service

In the Digg project add a reference to System.XML.Linq because I am calling a web service and am getting an XML document back (the RSS feed).

Add a new class called DiggService to the project that implements IDiggService

Visual Basic

Option Explicit On

Option Strict On

Imports System.Xml.Linq

Public Class DiggService

    Implements IDiggService

    Private searchCompleteCallback As Action(Of IEnumerable(Of DiggStory))

    Public Sub BeginSearch(ByVal query As String, ByVal SearchCompleteCallback As Action(Of IEnumerable(Of DiggStory))) Implements IDiggService.BeginSearch

        Me.searchCompleteCallback = SearchCompleteCallback

        ‘ Construct Digg REST URL

        Dim diggUrl As String = String.Format(http://services.digg.com/stories/topic/{0}?count=20&appkey=http%3A%2F%2Fscottgu.com”, query)

        ‘ Initiate Async Network call to Digg

        Dim diggService As New WebClient()

        AddHandler diggService.DownloadStringCompleted, AddressOf diggService_DownloadStringCompleted

        diggService.DownloadStringAsync(New Uri(diggUrl))

    End Sub

    Sub diggService_DownloadStringCompleted(ByVal sender As Object, ByVal e As DownloadStringCompletedEventArgs)

        searchCompleteCallback(BuildStories(e))

    End Sub

    Private Function BuildStories(ByVal e As DownloadStringCompletedEventArgs) As IEnumerable(Of DiggStory)

        If e.[Error] IsNot Nothing Then

            Return New List(Of DiggStory)(New DiggStory() {New DiggStory() With {.Title = e.[Error].Message}})

        End If

        Dim xmlStories As XDocument = XDocument.Parse(e.Result)

        Dim stories = From story In xmlStories.Descendants(“story”) _

                        Where story.Element(“thumbnail”) IsNot Nothing AndAlso _

                              Not story.<thumbnail>.Value.EndsWith(“.gif”) _

                        Select New DiggStory() With _

                        { _

                            .Id = CInt(story.@id), _

                            .Title = story.<title>.Value.Trim(), _

                            .Description = story.<description>.Value.Trim(), _

                            .ThumbNail = story.<thumbnail>.@src, _

                            .HrefLink = New Uri(story.@link), _

                            .NumDiggs = CInt(story.@diggs), _

                            .UserName = story.<user>.Value.Trim() _

                        }

        Return stories

    End Function

End Class

C#

using System;

using System.Net;

using System.Xml.Linq;

using System.Collections.Generic;

using System.Linq;

namespace NewsAggregator.Digg

{

    public class DiggService : IDiggService

    {

        public void BeginSearch(string query, Action<IEnumerable<DiggStory>> SearchCompleteCallback)

        {

            // Construct Digg REST URL

            string diggUrl = String.Format(http://services.digg.com/stories/topic/{0}?count=20&appkey=http%3A%2F%2Fscottgu.com”, query);

            // Initiate Async Network call to Digg

            WebClient diggService = new WebClient();

            diggService.DownloadStringCompleted += (sender, e) => SearchCompleteCallback(BuildStories(e));

            diggService.DownloadStringAsync(new Uri(diggUrl));

        }

        private IEnumerable<DiggStory> BuildStories(DownloadStringCompletedEventArgs e)

        {

            if (e.Error != null)

            {

                return new List<DiggStory> { new DiggStory() { Title = e.Error.Message } };

            }

            XDocument xmlStories = XDocument.Parse(e.Result);

            var stories = from story in xmlStories.Descendants(“story”)

                          where story.Element(“thumbnail”) != null &&

                                !story.Element(“thumbnail”).Attribute(“src”).Value.EndsWith(“.gif”)

                          select new DiggStory

                          {

                              Id = (int)story.Attribute(“id”),

                              Title = ((string)story.Element(“title”)).Trim(),

                              Description = ((string)story.Element(“description”)).Trim(),

                              ThumbNail = (string)story.Element(“thumbnail”).Attribute(“src”).Value,

                              HrefLink = new Uri((string)story.Attribute(“link”)),

                              NumDiggs = (int)story.Attribute(“diggs”),

                              UserName = (string)story.Element(“user”).Attribute(“name”).Value,

                          };

            return stories;

        }

    }

}

In the DiggModule register this search query in the UnityContainer. To acheive this, create a private IUnityContainer variable called container and add this to the constructor. You can then register this in the Initialize method. The ContainerControlledLifetimeManager class just indicates that the service should be a Singleton.

Visual Basic

Imports Microsoft.Practices.Composite.Modularity

Imports Microsoft.Practices.Composite.Regions

Imports Microsoft.Practices.Unity

Public Class DiggModule

    Implements IModule

    Private regionManager As IRegionManager

    Private container As IUnityContainer

    Public Sub New(ByVal regionManager As IRegionManager, ByVal container As IUnityContainer)

        Me.regionManager = regionManager

        Me.container = container

    End Sub

    Public Sub Initialize() Implements Microsoft.Practices.Composite.Modularity.IModule.Initialize

        Me.container.RegisterType(Of IDiggService, DiggService)(New ContainerControlledLifetimeManager())

        Me.regionManager.RegisterViewWithRegion(“ResultsRegion”, GetType(DiggSearchResultsView))

    End Sub

End Class

C#

using Microsoft.Practices.Composite.Modularity;

using Microsoft.Practices.Composite.Regions;

using Microsoft.Practices.Unity;

namespace NewsAggregator.Digg

{

    public class DiggModule : IModule

    {

        private IRegionManager regionManager;

        private IUnityContainer container;

        public DiggModule(IRegionManager regionManager, IUnityContainer container)

        {

            this.regionManager = regionManager;

            this.container = container;

        }

        #region IModule Members

        public void Initialize()

        {

            this.container.RegisterType<IDiggService, DiggService>(new ContainerControlledLifetimeManager());

            this.regionManager.RegisterViewWithRegion(“ResultsRegion”, typeof(DiggSearchResultsView));

        }

        #endregion

    }

}

If you run the project you should a view with live data

ConnectedToService

If you look carefully at the tab at the top of the shell you will see that there is not title for the shell so a way is needed to communicate title or header information back to the Shell

Header Information

There is a convention to follow when providing this information, and that is for the View Model to provide this information to the Shell Owner can look for this information. Add a new Property into the DiggSearchResultsViewModel called HeaderInfo.

Visual Basic

Imports System.Collections.ObjectModel

Public Class DiggSearchResultsViewModel

    Private _Stories As ObservableCollection(Of DiggStory)

    Private diggService As IDiggService

    Public Property Stories() As ObservableCollection(Of DiggStory)

        Get

            Return _Stories

        End Get

        Private Set(ByVal value As ObservableCollection(Of DiggStory))

            _Stories = value

        End Set

    End Property

    Public ReadOnly Property HeaderInfo() As String

        Get

            Return “Digg Search Results”

        End Get

    End Property

    Public Sub New(ByVal diggService As IDiggService)

        Stories = New ObservableCollection(Of DiggStory)()

        Dim story As New DiggStory()

        story.Title = “I am here, Digg it”

        Stories.Add(story)

        Me.diggService = diggService

        Me.diggService.BeginSearch(“baseball”, AddressOf OnSearchComplete)

    End Sub

    Private Sub OnSearchComplete(ByVal newStories As IEnumerable(Of DiggStory))

        Me.Stories.Clear()

        For Each diggStory In newStories

            Me.Stories.Add(diggStory)

        Next

    End Sub

End Class

C#

using System.Collections.ObjectModel;

using System.Collections.Generic;

namespace NewsAggregator.Digg

{

    public class DiggSearchResultsViewModel

    {

        private IDiggService diggService;

        public DiggSearchResultsViewModel(IDiggService diggService)

        {

            Stories = new ObservableCollection<DiggStory>();

            Stories.Add(new DiggStory(){Title = “I am here, Digg it”});

            this.diggService = diggService;

            this.diggService.BeginSearch(“baseball”, OnSearchComplete);

        }

        public string HeaderInfo

        {

            get { return “Digg Search Results”; }

        }

        private void OnSearchComplete(IEnumerable<DiggStory> newStories)

        {

            this.Stories.Clear();

            foreach (var diggStory in newStories)

            {

                this.Stories.Add(diggStory);

            }

        }

        public ObservableCollection<DiggStory> Stories

        {

            get;

            private set;

        }

    }

}

Open up the Shell.xaml and add the following markup in the tab regions section. Note in WPF this tab property is available to databind to directly, but this is not available in Silverlight.

<UserControl xmlns:basics=”clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls” x:Class=”NewsAggregator.Shell.Shell”

   xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation&#8221;

   xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml&#8221;

      xmlns:Regions=”clr-namespace:Microsoft.Practices.Composite.Presentation.Regions;assembly=Microsoft.Practices.Composite.Presentation”>

    <UserControl.Resources>

        <Style x:Key=”TopGrid” TargetType=”Grid”>

            <Setter Property=”Background” Value=”#FF5C7590″ />

        </Style>

    </UserControl.Resources>

    <Grid Style=”{StaticResource TopGrid}”>

        <Grid.RowDefinitions>

            <RowDefinition Height=”auto”/>

            <RowDefinition Height=”*”/>

        </Grid.RowDefinitions>

        <ContentControl Regions:RegionManager.RegionName=”SearchRegion” Grid.Row=”0″ Margin=”2″ />

        <basics:TabControl Regions:RegionManager.RegionName=”ResultsRegion” Grid.Row=”1″ Margin=”3″>

            <Regions:TabControlRegionAdapter.ItemContainerStyle>

                <Style TargetType=”basics:TabItem”>

                    <Setter Property=”HeaderTemplate”>

                        <Setter.Value>

                            <DataTemplate>

                                <TextBlock Text=”{Binding HeaderInfo}” />

                            </DataTemplate>

                        </Setter.Value>

                    </Setter>

                </Style>

            </Regions:TabControlRegionAdapter.ItemContainerStyle>

        </basics:TabControl>

    </Grid>

</UserControl>

If you run the program you should have

SearchResults

That wraps up this penultimate tutorial, in the fourth and final tutorial we will add a searchbox, and show how this can be added in a decoupled fashion.

The complete source code for the 4 part series is available here (Download the WPF Silverlight Prism Folder)

One thought on “Building a Composite WPF and Silverlight Application with Prism – Part 3

  1. Pingback: Building a Composite WPF and Silverlight Application with Prism - Part 4 « Ira Lukhezo’s blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s