Chapter 13. Presenters and Views

What You’ll Learn in This Hour:

  • Application level presenters

  • Screen-level presenters

  • Presenter/View communication

  • Dependency injection

With the Model we built in Hour 12 and an understanding of the MVP pattern, it’s time to complete the triad by building the first presenters and views of our application. Presenters are the home of application logic, and they often mediate between the View and the Model. Therefore, presenters are a core piece of the Contact Manager. They receive special attention this hour along with the patterns that enable them to communicate with views.

Creating a Custom Base Class for Presenters

In the process of developing this application, we will build several presenters for managing various aspects of the Contact Manager. There are a couple of things that all these presenters will have in common. These features make View/Presenter collaboration easier and will enable various aspects of the shell infrastructure that we are going to build. Let’s create a base class to implement the common presenter features:

  1. In the Contact Manager solution, create a new class in the Presenters folder called PresenterBase.cs.

  2. Use the code in Listing 13.1 to implement the class.

  3. Build the application to confirm that you have entered the code correctly.

Example 13.1. PresenterBase.cs

namespace ContactManager.Presenters
{
    public class PresenterBase<T> : Notifier
    {
        private readonly string _tabHeaderPath;
        private readonly T _view;

        public PresenterBase(T view)
        {
            _view = view;
        }

        public PresenterBase(T view, string tabHeaderPath)
        {
            _view = view;
            _tabHeaderPath = tabHeaderPath;
        }

        public T View
        {
            get { return _view; }
        }

        public string TabHeaderPath
        {
            get { return _tabHeaderPath; }
        }
    }
}

We have chosen to inherit our base class from Notifier. Recall from Hour 12, “Building a Contact Manager,” that this class gives us a basic implementation of INotifyPropertyChanged, which enables more powerful data binding in WPF. Besides data binding, presenters often need additional mechanisms to request information from the view or tell it to display something. The generic property T allows each presenter to talk to its associated view as necessary. Finally, the TabHeaderPath exists to support the tabbed UI infrastructure we are building. This property provides the data binding property path to bind to the Tab’s header.

Completing the Tab UI Infrastructure

We began developing the tabbed UI in Hour 12 by implementing the basic XAML in the Shell.xaml file. Before we can fully use it, we need to add a bit of “intelligence” to it. Let’s do that now.

  1. Open Shell.xaml.cs in the code editor.

  2. Add the following method to enable adding presenter-based views to the shell:

    public void AddTab<T>(PresenterBase<T> presenter)
    {
        TabItem newTab = null;
    
        for(int i = 0; i < tabs.Items.Count; i++)
        {
            TabItem existingTab = (TabItem) tabs.Items[i];
    
            if(existingTab.DataContext.Equals(presenter))
            {
                tabs.Items.Remove(existingTab);
                newTab = existingTab;
                break;
            }
        }
    
        if(newTab == null)
        {
            newTab = new TabItem();
    
            Binding headerBinding = new Binding(presenter.TabHeaderPath);
            BindingOperations.SetBinding(
                newTab,
                TabItem.HeaderProperty,
                headerBinding
                );
    
            newTab.DataContext = presenter;
            newTab.Content = presenter.View;
        }
    
        tabs.Items.Insert(0, newTab);
        newTab.Focus();
    }
  3. Add the following method to enable removing presenter-based views from the shell:

    public void RemoveTab<T>(PresenterBase<T> presenter)
    {
        for (int i = 0; i < tabs.Items.Count; i++)
        {
            TabItem item = (TabItem)tabs.Items[i];
    
            if (item.DataContext.Equals(presenter))
            {
                tabs.Items.Remove(item);
                break;
            }
        }
    }
  4. Make sure you include the following using statement at the top of the Shell.cs:

    using ContactManager.Presenters;
  5. Compile the application.

The AddTab method loops through the existing tabs to make sure that one is not already present for the given presenter. If no existing tab is found, one is created and a binding is set on the tab’s header using the TabHeaderPath. The new tab’s DataContext is set to the presenter and its Content is set to the associated view. Finally, the new or existing tab is inserted into the beginning of the list and given focus.

RemoveTab is quite simple. To remove a presenter’s tab, it searched through the list of tab items, comparing each DataContext (previously set in AddTab) to the presenter. When a match is found, the tab is removed.

Implementing the Application Presenter

In a typical application, there is a need for an Application Controller or Application Presenter. This special type of presenter provides common application-level functionality to other presenters and manages various other cross-presenter issues. We’ll build this now so that its capabilities will be available for future use:

  1. Add a new class to the Presenters folder called ApplicationPresenter.cs.

  2. Use the code in Listing 13.2 to implement this class.

  3. Open Shell.xaml.cs and change the constructor to the match the following:

    public Shell()
    {
        InitializeComponent();
        DataContext = new ApplicationPresenter(this, new ContactRepository());
    }
  4. Make sure to include the following using statement at the top of Shell.cs:

    using ContactManager.Model;
  5. Compile the application.

Example 13.2. ApplicationPresenter.cs

using System;
using System.Collections.ObjectModel;
using ContactManager.Model;

namespace ContactManager.Presenters
{
    public class ApplicationPresenter : PresenterBase<Shell>
    {
        private readonly ContactRepository _contactRepository;
        private ObservableCollection<Contact> _currentContacts;
        private string _statusText;

        public ApplicationPresenter(
            Shell view,
            ContactRepository contactRepository)
            : base(view)
        {
            _contactRepository = contactRepository;

            _currentContacts = new ObservableCollection<Contact>(
                _contactRepository.FindAll()
                );
        }

        public ObservableCollection<Contact> CurrentContacts
        {
            get { return _currentContacts; }
            set
            {
                _currentContacts = value;
                OnPropertyChanged("CurrentContacts");
            }
        }

        public string StatusText
        {
            get { return _statusText; }
            set
            {
                _statusText = value;
                OnPropertyChanged("StatusText");
            }
        }

        public void Search(string criteria)
        {
            if (!string.IsNullOrEmpty(criteria) && criteria.Length > 2)
            {
                CurrentContacts = new ObservableCollection<Contact>(
                    _contactRepository.FindByLookup(criteria)
                    );

                StatusText = string.Format(
                    "{0} contacts found.",
                    CurrentContacts.Count
                    );
            }
            else
            {
                CurrentContacts = new ObservableCollection<Contact>(
                    _contactRepository.FindAll()
                    );

                StatusText = "Displaying all contacts.";
            }
        }

        public void NewContact()
        {
            OpenContact(new Contact());
        }

        public void SaveContact(Contact contact)
        {
            if (!CurrentContacts.Contains(contact))
                CurrentContacts.Add(contact);

            _contactRepository.Save(contact);

            StatusText = string.Format(
                "Contact '{0}' was saved.",
                contact.LookupName
                );
        }

        public void DeleteContact(Contact contact)
        {
            if (CurrentContacts.Contains(contact))
                CurrentContacts.Remove(contact);

            _contactRepository.Delete(contact);

            StatusText = string.Format(
                "Contact '{0}' was deleted.",
                contact.LookupName
                );
        }

        public void CloseTab<T>(PresenterBase<T> presenter)
        {
            View.RemoveTab(presenter);
        }

        public void OpenContact(Contact contact)
        {
            throw new NotImplementedException();
        }

        public void DisplayAllContacts()
        {
            throw new NotImplementedException();
        }
    }
}

The ApplicationPresenter exposes the necessary functionality that several components (views and other presenters) will need to get their work done. Notice that the method names on the presenter mostly reflect the features we said we would support at the beginning of Hour 12. These features include things such as searching contacts and creating, saving, and deleting contacts. The CloseTab method will be used by other presenters as well as the OpenContact method.

Some important architectural decisions were made in the implementation of this class and how it works into the overall application design. Let’s mention a few:

  • Notice that the constructor is dependent on two other classes: an instance of Shell and an instance of ContactRepository. This technique is known as constructor injection and is a specific type of dependency injection. DI is a technique that is commonly used to help enforce the principle of Separation of Concerns (SoC), which we have mentioned on several occasions. Dependency injection allows the presenter to worry about application-level functionality and to easily interact with other classes to accomplish complex tasks.

  • Examine the Search method. This is a great example of how the ApplicationPresenter adds application-level logic to the low-level search capabilities of the repository. The presenter should not know the details of how search is accomplished, but it is responsible for determining under what conditions it should occur.

  • The new, save, and delete methods follow the same pattern as Search. They are concerned only with the application-level issues surrounding the given command. For all other issues, they delegate to the repository.

  • The CloseTab method on this presenter allows other child presenters to close themselves by user request. Notice how the ApplicationPresenter forwards this request on to the View. The presenter, essentially, does not care how the view removes the tab; this is a view’s responsibility. It simply asks for it to be done.

  • Finally, we instantiate the presenter and set it to the DataContext property on the Shell. This enables data binding for the CurrentContacts collection and the StatusText property. Additionally, this presenter will be available to the user controls SideBar and SearchBar because they will automatically inherit the DataContext value from their parent.

Enhancing the Shell

Now that we have some application functionality brewing, we need a way to access it through the user interface. In Hour 12 we created two stub user controls that we plugged into the Shell. Let’s build out one of them a little more now:

  1. Open SideBar.xaml and alter the markup to match the following:

    <UserControl x:Class="ContactManager.UserControls.SideBar"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <DockPanel>
            <StackPanel DockPanel.Dock="Bottom"
                        Margin="4">
                <Button Content="New Contact"
                        Click="New_Click" />
                <Button Content="View All"
                        Click="ViewAll_Click" />
            </StackPanel>
            <ScrollViewer VerticalScrollBarVisibility="Auto">
                <ItemsControl Width="250"
                              VerticalAlignment="Stretch"
                              BorderThickness="0"
                              ItemsSource="{Binding CurrentContacts}">
                </ItemsControl>
            </ScrollViewer>
        </DockPanel>
    </UserControl>
  2. Open SideBar.xaml.cs and alter its code according to the listing that follows:

    using System.Windows;
    using System.Windows.Controls;
    using ContactManager.Presenters;
    
    namespace ContactManager.UserControls
    {
        public partial class SideBar : UserControl
        {
            public SideBar()
            {
                InitializeComponent();
            }
    
            public ApplicationPresenter Presenter
            {
                get { return DataContext as ApplicationPresenter; }
            }
    
            private void New_Click(object sender, RoutedEventArgs e)
            {
                Presenter.NewContact();
            }
            private void ViewAll_Click(object sender, RoutedEventArgs e)
            {
                Presenter.DisplayAllContacts();
            }
        }
    }
  3. Run the application to see the results of the changes.

We mentioned in the previous section that the ApplicationPresenter would be inherited through the DataContext property. We are taking advantage of that here. Because of our application architecture, the view’s button clicks can delegate to the presenter for appropriate action.

ScrollViewer

The SideBar uses a ScrollViewer control to create a scrollable region inside the view. ScrollViewer is a ContentControl. It is capable of providing a scrollable interface for anything set to its Content property. This control provides several special properties. Two that are most commonly used are VerticalScrollBarVisibility and HorizontalScrollBarVisibility. These properties allow you to define when the scrollbars will become visible. Setting the value to Auto keeps the scrollbars hidden until the Content is too large for the display area and requires scrolling.

ItemsControl

The ItemsControl used in the preceding markup has been mentioned at several points in previous hours. It is the base class for more complex list controls such as ListBox, ComboBox, and TabControl. An ItemsControl implements the minimum code necessary to display a list of items. We’ll discuss this control and its inheritors in great detail in Hour 16, “Visualizing Lists.”

Building an Edit Contact Screen

Running the Contact Manager and clicking New Contact will result in an exception being thrown because we haven’t implemented the edit contact screen. Implementing this piece requires creating a presenter, view, and making some minor additions to the ApplicationPresenter. Let’s get to work:

  1. Create a new UserControl called EditContactView.xaml in the Views folder.

  2. Use the markup in Listing 13.3 to implement this view.

  3. Add a new class to the Presenters folder called EditContactPresenter.cs.

  4. Use the code in Listing 13.4 to implement this class.

  5. Open the EditContactView.xaml.cs and make it match the code in Listing 13.5.

  6. Open the ApplicationPresenter and change the OpenContact method to the following:

    public void OpenContact(Contact contact)
    {
        if (contact == null) return;
    
        View.AddTab(
            new EditContactPresenter(
                this,
                new EditContactView(),
                contact
                )
            );
    }
  7. Run the application. Click New Contact. Enter some contact details and save. Try using the zoom slider. You should be able to see the effects more clearly than before.

Example 13.3. EditContactView.xaml

<UserControl x:Class="ContactManager.Views.EditContactView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <DockPanel Margin="5">
        <Border DockPanel.Dock="Top">
            <DockPanel LastChildFill="False">
                <TextBlock DockPanel.Dock="Left"
                           Text="{Binding Contact.LastName}" />
                <TextBlock DockPanel.Dock="Left"
                           Text=", " />
                <TextBlock DockPanel.Dock="Left"
                           Text="{Binding Contact.FirstName}" />
                <TextBlock DockPanel.Dock="Right"
                           Text="{Binding Contact.Organization}" />
            </DockPanel>
        </Border>

        <StackPanel DockPanel.Dock="Bottom">
            <Button Content="Save"
                    Click="Save_Click" />
            <Button Content="Delete"
                    Click="Delete_Click" />
            <Button Content="Close"
                    Click="Close_Click" />
        </StackPanel>
        <WrapPanel>
            <GroupBox>
                <GroupBox.Header>
                    <Border>
                        <TextBlock Text="General" />
                    </Border>
                </GroupBox.Header>

                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="100" />
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="175" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <Grid Grid.RowSpan="4">
                        <Border Background="Gray"
                                CornerRadius="6"
                                Margin="2 2 0 0"
                                Opacity=".5" />
                        <Border Margin="2 2 4 4"
                                Background="White" />
                        <Viewbox Margin="2 2 4 4">
                            <Image Source="{Binding Contact.ImagePath}" />
                        </Viewbox>
                        <Border BorderThickness="2"
                                Background="Transparent"
                                CornerRadius="6"
                                Margin="0 0 2 2" />
                        <Button Background="White"
                                Click="SelectImage_Click" />
                    </Grid>

                    <Label Grid.Column="1"
                           Content="_First Name:"
                           Target="{Binding ElementName=firstName}" />
                    <TextBox x:Name="firstName"
                             Grid.Column="2"
                             Text="{Binding Contact.FirstName}" />

                    <Label Grid.Row="1"
                           Grid.Column="1"
                           Content="_Last Name:"
                           Target="{Binding ElementName=lastName}" />
                    <TextBox x:Name="lastName"
                             Grid.Row="1"
                             Grid.Column="2"
                             Text="{Binding Contact.LastName}" />
                    <Label Grid.Row="2"
                           Grid.Column="1"
                           Content="Or_ganization:"
                           Target="{Binding ElementName=organization}" />
                    <TextBox x:Name="organization"
                             Grid.Row="2"
                             Grid.Column="2"
                             Text="{Binding Contact.Organization}" />

                    <Label Grid.Row="3"
                           Grid.Column="1"
                           Content="_Job Title:"
                           Target="{Binding ElementName=jobTitle}" />
                    <TextBox x:Name="jobTitle"
                             Grid.Row="3"
                             Grid.Column="2"
                             Text="{Binding Contact.JobTitle}" />
                </Grid>
            </GroupBox>
        </WrapPanel>
    </DockPanel>
</UserControl>

Example 13.4. EditContactPresenter.cs

using ContactManager.Model;
using ContactManager.Views;

namespace ContactManager.Presenters
{
    public class EditContactPresenter : PresenterBase<EditContactView>
    {
        private readonly ApplicationPresenter _applicationPresenter;
        private readonly Contact _contact;

        public EditContactPresenter(
            ApplicationPresenter applicationPresenter,
            EditContactView view,
            Contact contact)
            : base(view, "Contact.LookupName")
        {
            _applicationPresenter = applicationPresenter;
            _contact = contact;
        }

        public Contact Contact
        {
            get { return _contact; }
        }

        public void SelectImage()
        {
            string imagePath = View.AskUserForImagePath();
            if (!string.IsNullOrEmpty(imagePath))
                Contact.ImagePath = imagePath;
        }

        public void Save()
        {
            _applicationPresenter.SaveContact(Contact);
        }

        public void Delete()
        {
            _applicationPresenter.CloseTab(this);
            _applicationPresenter.DeleteContact(Contact);
        }

        public void Close()
        {
            _applicationPresenter.CloseTab(this);
        }

        public override bool Equals(object obj)
        {
            EditContactPresenter presenter = obj as EditContactPresenter;
            return presenter != null && presenter.Contact.Equals(Contact);
        }
    }
}

Example 13.5. EditContactView.xaml.cs

using System.Windows;
using System.Windows.Controls;
using ContactManager.Presenters;
using Microsoft.Win32;

namespace ContactManager.Views
{
    public partial class EditContactView : UserControl
    {
        public EditContactView()
        {
            InitializeComponent();
        }

        public EditContactPresenter Presenter
        {
            get { return DataContext as EditContactPresenter; }
        }

        private void Save_Click(object sender, RoutedEventArgs e)
        {
            Presenter.Save();
        }
        private void Delete_Click(object sender, RoutedEventArgs e)
        {
            Presenter.Delete();
        }

        private void Close_Click(object sender, RoutedEventArgs e)
        {
            Presenter.Close();
        }

        private void SelectImage_Click(object sender, RoutedEventArgs e)
        {
            Presenter.SelectImage();
        }

        public string AskUserForImagePath()
        {
            OpenFileDialog dlg = new OpenFileDialog();
            dlg.ShowDialog();
            return dlg.FileName;
        }
    }
}

Although the Contact Manager doesn’t look very attractive (see Figure 13.1), it is now functioning and supported by a solid architecture.

A functioning Contact Manager.

Figure 13.1. A functioning Contact Manager.

We’ve introduced a lot of code and markup in the previous section, but most of it is pretty straightforward. Looking at the EditContactView.xaml, you will find pretty typical markup compared to what we’ve been working with in previous chapters. In the next hour, we are going to make some changes to this markup that greatly beautify it. For now, it works.

EditContactPresenter follows similar patterns as ApplicationPresenter does. Notice how it takes an instance of the application presenter in its constructor. This is essential to this particular implementation of the MVP pattern. Much of what the presenter does is delegated to the application presenter; the rest, such as determining a filename, is delegated to the view. Pay special attention to the override of Equals. Our tab infrastructure requires this to prevent multiple copies of the same contact being open at the same time.

Did you Know?

When you compile this application, you may get several compiler warnings, depending on your Visual Studio settings. These warnings are related to the overriding of the Equals method on several classes. In a real-world application, you would want to override GetHashCode, the == operator, and the != operator anytime you override Equals. Because it is not necessary for our example, we have left these implementations out for the purpose of simplification.

When it comes to the EditContactView.xaml.cs code-behind, we have followed the same pattern as in previous examples. The one interesting thing to take note of is the AskUserForImagePath method. This allows the presenter to ask the view for this information without needing to be concerned with how it is done. In this case, the view uses an OpenFileDialog to locate an image, but it could have used any number of means.

Summary

This hour has introduced the final important pieces into our solution; presenters and views. We’ve looked at a lot of code and markup, but we’ve seen how a well-thought-out architecture can keep code simple and isolated. Separation of Concerns has played a key role in our design, enabling repository logic to be separated from application-wide logic, which in turn is separated from screen-specific logic.

Q&A

Q.

MVC and MVP are interesting and useful patterns. Where can I go to find more examples of this being used in the context of WPF?

A.

Sadly, there isn’t much literature available on implementing these patterns in WPF yet. More and more developers are seeing the benefits of designing applications in this fashion, so there is likely to be more guidance soon. In the meantime, investigate how other platforms have tackled these issues.

Q.

Why doesn’t WPF have more built-in support for the MVP pattern?

A.

The creators of WPF, in trying to cater to a very wide range of customers, have left these issues out of the core design of WPF. In doing this, they have given developers the flexibility to solve complex UI problems in a variety of ways. The method presented in this book is one of several variations.

Workshop

Quiz

1.

Describe the role of an application controller/presenter.

2.

Describe the role of a subordinate or screen-related presenter.

Answers

1.

An application presenter is responsible for managing items that have application-wide scope. This could be state that is accessed or manipulated by various parts of the application, or it could be UI infrastructure methods that other presenters require to function properly. Examples of what such methods might enable are adding tabs to a central view and allowing a presenter to add custom menu options or toolbar buttons.

2.

All other presenters besides the application presenter tend to function in a subordinating role. They exist to help the application presenter do its job. Some may function by separating out additional application-wide responsibilities, whereas others may exist to manage the presentation of a specific screen within the application.

Activities

  1. Research “Martin Fowler,” if you have not already. Browse his website and read up on some common UI related patterns.

  2. Extract interfaces for the existing views and presenters. Alter the constructors of the presenters to depend on interfaces rather than concrete classes. In a real-world application, all code would work against interfaces rather than concrete classes. This enables further decoupling and ease of testing throughout the application.

  3. Spend some time on the Internet researching “dependency injection” and the related term “inversion of control.” Several open source frameworks ease the use of these techniques: StructureMap, Windsor, and Spring.net. You may find it valuable to investigate these tools because they are likely to be of great use on any medium or large projects you may develop.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.226.255