Aracılığıyla paylaş


Using a separated presentation pattern in Hilo (Windows Store apps using JavaScript and HTML)

[This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation]

From: Developing an end-to-end Windows Store app using JavaScript: Hilo

Previous page | Next page

In Hilo, we used the supervising controller pattern (a Model-View-Presenter, or MVP, pattern) to get HTML templating support. The supervising controller pattern helped to separate the view from the presenter responsibilities. In most Hilo pages, we also used the mediator pattern to separate and coordinate presentation responsibilities. This separation allowed the app to be tested more easily and the code is also easier to understand.

Download

After you download the code, see Getting started with Hilo for instructions.

You will learn

  • How Windows Store apps using JavaScript can benefit from MVP.
  • Recommended techniques for applying the MVP pattern: the supervising controller and the mediator pattern.
  • How to isolate responsibilities of the presenter classes.
  • How to share views across pages for common UI elements such as the Back button and title.

Applies to

  • Windows Runtime for Windows 8
  • Windows Library for JavaScript
  • JavaScript

MVP and the supervising controller pattern

The MVP pattern separates business logic, UI, and presentation behavior.

  • The model represents the state and operations of business objects that your app manipulates.
  • The view (HTML and CSS) defines the structure, layout, and appearance of what the user sees on the screen. The view manages the controls on the page and forwards user events to a presenter class.
  • The presenter contains the logic to respond to events, update the model and, in turn, manipulate the state of the view.

As you make the UI for a JavaScript app more declarative in nature, it becomes easier to separate the responsibilities of the view from presenter classes, and the view can interact directly with the data of the application (the model) through data binding. These are the main features of the supervising controller pattern, which is a type of MVP pattern. For more info, see Supervising controller on Martin Fowler's Website. For more info on MVP, see Model-View-Presenter pattern.

In Hilo, the controller corresponds to the presenter in MVP. To clarify meaning, we will use the term supervising presenter when discussing this pattern.

In the supervising presenter pattern, the view directly interacts with the model to perform simple data binding that can be declaratively defined, without presenter intervention. The model has no knowledge of the view. The presenter updates the model. It manipulates the state of the view only when complex UI logic that cannot be specified declaratively is required.

Tip  If your app needs to support changes to the model, you may need to implement an observer in your app. WinJS has the ability to update the view for changes in the model through binding, but not vice versa. You can use the observableMixin object to implement this.

 

Here are the relationships between the view, the model, and the presenter in this pattern.In the diagram, the blue lines represent indirect references. The blue line from the model to the view represents declarative data binding. The blue line from the model to the presenter represents event activation.

We implemented the supervising presenter pattern in Hilo. We chose this implementation instead of the passive view (another MVP pattern) to get HTML templating support. This made it easier to separate the view and the presenter responsibilities. In this instance, we chose simplicity of code over testability. Nevertheless, testability was an important concern for the project. For example, we also opted to create a presenter for every control on a page. This gave us more testable code, and we also liked the clean separation of concerns that this choice provided. We felt that the assignment of clear, explicit roles made the extra code that was required worthwhile.

Note  We did not implement the Model-View-ViewModel pattern (MVVM) because two-way data binding is not supported.

 

The presenter classes in Hilo contain the logic to respond to the events, update the model and, in turn, manipulate the state of the view if needed. In Hilo, presenter classes are specified by using file name conventions. For example, the presenter class for the hub page's ListView control is implemented in listViewPresenter.js and the presenter class for the FlipView control is implemented in flipviewPresenter.js. The presenter class names correspond to the file names.

In Hilo, we used WinJS templates for declarative binding, such as the template example shown here. This template is attached to a ListView control (not shown) to display images in the hub page. The url, name, and className properties for each image are declaratively bound to the model. For more info on templates, see Using Controls.

Hilo\Hilo\hub\hub.html

<div id="hub-image-template" data-win-control="WinJS.Binding.Template">
    <div data-win-bind="style.backgroundImage: url.backgroundUrl; alt: name; className: className" class="thumbnail">
    </div>
</div>

To facilitate testing of the presenter classes, the presenter in the MVP pattern has a reference to the view interface instead of a concrete implementation of the view. In this pattern, you can easily replace the real view with a mock implementation of the view to run tests. This is the approach we take in unit testing Hilo. For more info, see Testing and deployment. In the test code below, we create a ListViewPresenter with a reference to mock control (Specs.WinControlStub) instead of an actual UI control.

Hilo\Hilo.Specfications\specs\hub\ListViewPresenter.spec.js

describe("when snapped", function () {

    var el;

    beforeEach(function () {
        var appView = {};
        el = new Specs.WinControlStub();
        el.winControl.addEventListener = function () { };

        var listviewPresenter = new Hilo.Hub.ListViewPresenter(el, appView);
        listviewPresenter.setViewState(Windows.UI.ViewManagement.ApplicationViewState.snapped);
    });

    it("the ListView should use a ListLayout", function () {
        expect(el.winControl.layout instanceof WinJS.UI.ListLayout).equal(true);
    });

});

[Top]

Mediator pattern

In most Hilo pages, the mediator pattern is used to separate and coordinate presentation concerns. In the mediator pattern, we use a mediator to coordinate behavior of multiple objects (colleagues) that communicate with each other only indirectly, through the mediator object. This enables a loose coupling of the presenters. Each colleague object has a single responsibility.

Each presenter is responsible for a specific part of the page (a control) and the associated behaviors. One page-specific presenter (the mediator) coordinates the other presenters (control-specific) and receives forwarded events. In most of the Hilo pages, we obtain DOM elements in the page’s ready function, by using document.querySelector. We then pass the DOM elements to the control-specific presenter. For example, the UI for the detail page contains an AppBar, a filmstrip (based on a ListView control), and a FlipView control. In the ready function for detail.js, we obtain the DOM elements for each control, and then pass each element to a new instance of each respective presenter.

Hilo\Hilo\detail\detail.js

ready: function (element, options) {

    var query = options.query;
    var queryDate = query.settings.monthAndYear;
    var pageTitle = Hilo.dateFormatter.getMonthFrom(queryDate) + " " + Hilo.dateFormatter.getYearFrom(queryDate);
    this.bindPageTitle(pageTitle);

    var hiloAppBarEl = document.querySelector("#appbar");
    var hiloAppBar = new Hilo.Controls.HiloAppBar.HiloAppBarPresenter(hiloAppBarEl, WinJS.Navigation, query);

    var filmstripEl = document.querySelector("#filmstrip");
    var flipviewEl = document.querySelector("#flipview");

    var flipviewPresenter = new Hilo.Detail.FlipviewPresenter(flipviewEl);
    var filmstripPresenter = new Hilo.Detail.FilmstripPresenter(filmstripEl);


    var detailPresenter = new Hilo.Detail.DetailPresenter(filmstripPresenter, flipviewPresenter, hiloAppBar, WinJS.Navigation);
    detailPresenter.addEventListener("pageSelected", function (args) {
        var itemIndex = args.detail.itemIndex;
        options.itemIndex = itemIndex;
    });

    detailPresenter
        .start(options)
        .then(function () {
            WinJS.Application.addEventListener("Hilo:ContentsChanged", Hilo.navigator.reload);
        });
},

Once the control-specific presenters are created, we pass them to a new instance of the page-specific presenter.

Note  The ready function is part of the navigation model, and is called automatically when the user navigates to a page control. In Hilo, we use the page controls to obtain DOM elements and instantiate dependencies, but the page controls do not correspond to a specific part of MVP. For more info, see Creating and navigating between pages.

 

WinJS.Namespace.define exposes the presenter objects for use in the app. Here is the code that does this for the detail page presenter.

Hilo\Hilo\detail\detailPresenter.js

WinJS.Namespace.define("Hilo.Detail", {
    DetailPresenter: WinJS.Class.mix(DetailPresenter, WinJS.Utilities.eventMixin)
});

When the detail page presenter is created, its constructor function assigns the control-specific presenters to detail page presenter properties. This pattern is typical for the various Hilo presenter classes.

Hilo\Hilo\detail\detailPresenter.js

function DetailPresenterConstructor(filmstripPresenter, flipviewPresenter, hiloAppBar, navigation) {
    this.flipview = flipviewPresenter;
    this.filmstrip = filmstripPresenter;
    this.hiloAppBar = hiloAppBar;
    this.navigation = navigation;

    Hilo.bindFunctionsTo(this, [
        "bindImages"
    ]);
},

We invoke the presenter’s start function from ready. The start function executes a query to obtain the images needed, and then calls bindImages, passing it the query results. For more info on file system queries in Hilo, see Using the query builder pattern.

Hilo\Hilo\detail\detailPresenter.js

start: function (options) {
    var self = this;
    this.query = options.query;

    return this.query.execute()
        .then(function (images) {

            var result = findImageByIndex(images, options.itemIndex, options.itemName);
            var storageFile = images[options.itemIndex];
            // If the file retrieved by index does not match the name associated
            // with the query, we assume that it has been deleted (or modified)
            // and we send the user back to the last screen.
            if (isNaN(result.actualIndex)) {
                self.navigation.back();
            } else {
                self.bindImages(images);
                self.gotoImage(result.actualIndex, options.picture);
            }
        });

},

The bindImages function in detailPresenter.js registers event handlers for events such as imageInvoked. The handler receives events dispatched from the control-specific presenter classes. In the details page, the behavior is the same whether the user clicks an image in the filmstrip or the FlipView control, so the event handling is coordinated here in the page's mediator.

Hilo\Hilo\detail\detailPresenter.js

bindImages: function (images) {

    this.flipview.bindImages(images);
    this.flipview.addEventListener("pageSelected", this.imageClicked.bind(this));

    this.filmstrip.bindImages(images);
    this.filmstrip.addEventListener("imageInvoked", this.imageClicked.bind(this));

    this.hiloAppBar.enableButtons();
},

To bind the images to the control, bindImages calls the bindImages function in the control-specific presenters. The images are bound to the control by using the control’s itemDataSource property.

Hilo\Hilo\detail\flipViewPresenter.js

bindImages: function (images) {
    this.bindingList = new WinJS.Binding.List(images);
    this.winControl.itemDataSource = this.bindingList.dataSource;
},

For more info on binding to data sources, see Using Controls.

[Top]

Separating responsibilities for the AppBar and the page header

For common page elements, such the AppBar and the page header (an HTMLControl), we implemented re-usable controls that could be used on multiple pages. We moved the files for these controls out of the page-related subfolders into the \Hilo\controls folder. The AppBar controls are defined in the Hilo\controls\HiloAppBar folder, and the page header control is defined in the Hilo\controls\Header folder. The page header includes a Back button control in addition to a page header. Moving the presenter classes for these controls away from page-related code helped us to cleanly separate concerns.

Both the AppBar and the page header are defined as page controls that support the recommended navigation model for a Windows Store app. To define them as page controls, we specify the use of the HTMLControl in the HTML code and make a call to WinJS.UI.Page.define in the associated JavaScript.

In the Hilo implementation, the HTML for a particular page only needs a section reference to the AppBar or Back button control. For example, here is the HTML used for the AppBar in the hub page and details page.

Hilo\Hilo\hub\hub.html

<section id="image-nav" data-win-control="WinJS.UI.HtmlControl" data-win-options="{uri: '/Hilo/controls/HiloAppBar/hiloAppBar.html'}"></section>

The main view for the AppBar is contained in the HTML page referenced here, hiloAppBar.html. The HTML code here specifies two buttons for the AppBar, one for the crop command, and one for the rotate command.

Hilo\Hilo\controls\HiloAppBar\hiloAppBar.html

<div id="appbar" data-win-control="WinJS.UI.AppBar" data-win-options="{sticky: false}">
    <button
        data-win-control="WinJS.UI.AppBarCommand"
        data-win-options="{id:'rotate', icon:'rotate', section: 'selection', disabled: true}"
        data-win-bind="{disabled: isCorrupt}"
        data-win-res="{winControl: {label:'RotateAppBarButton.Name'}}">
    </button>
    <button
        data-win-control="WinJS.UI.AppBarCommand"
        data-win-options="{id:'crop', icon:'crop', section: 'selection', disabled: true}"
        data-win-bind="{disabled: isCorrupt}"
        data-win-res="{winControl: {label:'CropAppBarButton.Name'}}">
    </button>
</div>

The CSS and JavaScript files associated with hiloAppBar.html are contained in the same project location, the Hilo\controls\HiloAppBar folder.

[Top]

Separating responsibilities for the ListView

For pages with ListView controls, like the hub view, we couldn’t easily separate the view associated with the ListView from the page itself, because the ListView is generally closely tied to whole page behavior and UI. Instead, we created a ListView presenter for each page to help separate concerns. For example, the hub page has a ListViewPresenter class specified in listViewPresenter.js, which is found in the \Hilo\hub folder along with other hub-related files.

The ListViewPresenter class handles all state and events associated with the ListView control for a particular page. For example, we set view state and we set the data source for the ListView in the ListViewPresenter. Events received by the ListView are forwarded to the page’s presenter class. In the hub page, we use the imageNavigated event handler to handle the ListView's iteminvoked event. We then raise a new event to be handled generically in the HubPresenter.

Hilo\Hilo\hub\listViewPresenter.js

imageNavigated: function (args) {
    var self = this;
    args.detail.itemPromise.then(function (item) {
        self.dispatchEvent("itemInvoked", {
            item: item,
            itemIndex: args.detail.itemIndex
        });
    });
},

The raised event gets handled by the itemClicked function in the HubPresenter (not shown).

[Top]