Mastodon

Wednesday, March 25, 2015

Decoupling Views In Multi-Screen Sequences

In my previous post I explained how to decouple individual views and why that is a good idea. In this post I will take that idea further and explain how to use this concept in more advanced UX scenarios involving multi-screen sequences.

Motivation

As a summary, the benefits of decoupling views are increased flexibility and allowing for more code reuse. For instance, a particular type of view may be used in multiple parts of your application in slightly different scenarios. If that view makes assumptions about where it fits within the whole app then it would be difficult to reuse that view in a different part of the app.

Still, at some level in your application you need to build in some kind of knowledge of which view is next. In the last post I gave a basic example where that knowledge lived in the Application class. There are many situations in which the Application class may be the best place for this kind of app-wide navigation logic, but some situations are more advanced and require a more sophisticated technique.

For example, it is also common to have a series of views within an app that always go together, but that sequence as a whole may be launched from different parts of the application. On iOS this kind of reusable sequence of views can be represented in a Storyboard1, but we can achieve the same result in code.

An Example

As an example let's consider a sequence of views for posting a picture to a social network:

  1. Choose a picture from a library or choose to take a new picture.
  2. If the user chose to take a new picture then show the camera view.
  3. After the user has either chosen a picture or taken a new picture he can add a comment.
  4. The picture is posted.

At any point during this process the user should also have the option to cancel, which should return the user back to where he started.

Here are some questions to consider when implementing this UX flow:

  • How can we handle the cancel button in a way that avoids code duplication?
  • How can we avoid code duplication for the various parts of the app that might want to invoke this sequence? For instance, perhaps you can post a picture either to your own profile or on someone else's profile or in a comment or in a private message.
  • How can we allow for flexibility such that different parts of the app can do different things with the chosen picture/comment?

The first two questions are about code reuse, which is one of our goals. We want to avoid both having these individual screens duplicate code to accomplish the same thing, and we also want to avoid duplication of code from elsewhere in our app. The last question is about how we can decouple this code itself from the act of using the results of the sequence (i.e., the picture and the comment). This is important because each part of the app that might use this probably has to do slightly different things with the results.

Creating the Views

The example flow has three unique screens:

  1. A screen that lets the user choose an image or choose to take a new picture.
  2. A screen for taking a picture.
  3. A screen for entering a comment.

As per my last post, each of these views should be written to be agnostic about how it's used. There may be yet another part of the application that allows for editing a comment on an existing post, and you probably want to reuse the same view (#3) for that use case. Therefore you shouldn't make any assumptions when implementing that view about how it will be used.

To accomplish this each view could be written with events for getting the results. Their APIs might look like this:

public class ImageEventArgs : EventArgs
{
    public Image Image { get; private set; }

    public ImageEventArgs(Image image)
    {
        Image = image;
    }
}

public class CommentEventArgs : EventArgs
{
    public string Comment { get; private set; }

    public CommentEventArgs(string comment)
    {
        Comment = comment;
    }
}

public class ImagePickerPage : ContentPage
{
    public event EventHandler TakeNewImage;

    public event EventHandler<ImageEventArgs> ImageChosen;

    // ...
}

public class CameraPage : ContentPage
{
    public event EventHandler<ImageEventArgs> PictureTaken;

    // ...
}

public class ImageCommentPage : ContentPage
{
    public event EventHandler<CommentEventArgs> CommentEntered;

    public ImageCommentPage(Image image)
    {
        // ...
    }

    // ...
}

Constructing the Sequence

Now that we have our building blocks we need to put it all together. To do that we will create a new class that represents the whole sequence. This new class doesn't need to be a view itself. Instead, it is just an object that manages the sequence. It will be responsible for creating each page as needed, putting them on the screen, and combining the results. Its public API might look like this:

public class CommentedImageSequenceResults
{
    public static CommentedImageSequenceResults CanceledResult = new CommentedImageSequenceResults();

    public bool Canceled { get; private set; }

    public Image Image { get; private set; }

    public string Comment { get; private set; }

    public CommentedImageSequenceResults(Image image, string comment)
    {
        Image = image;
        Comment = comment;
    }

    private CommentedImageSequenceResults()
    {
        Canceled = true;
    }
}

public class CommentedImageSequence
{
    public static Task<CommentedImageSequenceResults> ShowAsync(INavigation navigation)
    {
        // ...
    }

    // ...
}

Notice that in this case I've chosen to simplify the API by using a Task<T> instead of multiple events. This plays nicely with C#'s async/await feature. I could have done the same with each of the individual views as well, but I wanted to show both approaches. Here is an example of how this API could be used:

public class ProfilePage : ContentPage
{
    // ...

    private async void HandleAddImageButtonPressed(object sender, EventArgs e)
    {
        var results = await CommentedImageSequence.ShowAsync(Navigation);
        if (!results.Canceled)
        {
            PostImage(results.Image, results.Comment);
        }
    }
}

Of course you could have similar code elsewhere in the app, but what you do with the results would be different. That satisfies our requirements of flexibility and avoiding code duplication.

Now let's look at how you would actually implement the sequence:

public class CommentedImageSequence
{
    private readonly TaskCompletionSource<CommentedImageSequenceResults> _taskCompletionSource = new TaskCompletionSource<CommentedImageSequenceResults>();

    private readonly NavigationPage _navigationPage;
    private readonly ToolbarItem _cancelButton;

    private Image _image;

    private CommentedImageSequence()
    {
        _cancelButton = new ToolbarItem("Cancel", icon: null, activated: HandleCancel);
        _navigationPage = new NavigationPage(CreateImagePickerPage());
    }

    private void AddCancelButton(Page page)
    {
        page.ToolbarItems.Add(_cancelButton);
    }

    private ImagePickerPage CreateImagePickerPage()
    {
        var page = new ImagePickerPage();
        AddCancelButton(page);
        page.TakeNewImage += HandleTakeNewImage;
        page.ImageChosen += HandleImageChosen;
        return page;
    }

    private CameraPage CreateCameraPage()
    {
        var page = new CameraPage();
        AddCancelButton(page);
        page.PictureTaken += HandleImageChosen;
        return page;
    }

    private ImageCommentPage CreateImageCommentPage()
    {
        var page = new ImageCommentPage(_image);
        AddCancelButton(page);
        page.CommentEntered += HandleCommentEntered;
        return page;
    }

    private async void HandleTakeNewImage(object sender, EventArgs e)
    {
        await _navigationPage.PushAsync(CreateCameraPage());
    }

    private async void HandleImageChosen(object sender, ImageEventArgs e)
    {
        _image = e.Image;
        await _navigationPage.PushAsync(CreateImageCommentPage());
    }

    private void HandleCommentEntered(object sender, CommentEventArgs e)
    {
        _taskCompletionSource.SetResult(new CommentedImageSequenceResults(_image, e.Comment));
    }

    private void HandleCancel()
    {
        _taskCompletionSource.SetResult(CommentedImageSequenceResults.CanceledResult);
    }

    public static async Task<CommentedImageSequenceResults> ShowAsync(INavigation navigation)
    {
        var sequence = new CommentedImageSequence();

        await navigation.PushModalAsync(sequence._navigationPage);

        var results = await sequence._taskCompletionSource.Task;

        await navigation.PopModalAsync();

        return results;
    }
}

Let's summarize what this class does:

  1. It creates the NavigationPage used for displaying the series of pages and allowing the user to go back, and it presents that page (modally).
  2. It creates the cancel button that allows the user to cancel. Notice how only one cancel button needed to be created, and it is handled in only one place. Code reuse!
  3. It creates each page in the sequence as needed and pushes it onto the NavigationPage's stack.
  4. It keeps track of all of the information gathered so far. That is, once a user has taken or captured an image it holds onto that image while waiting for the user to enter a comment. Once the comment is entered it can return both the image and the comment together.
  5. It dismisses everything when done.

Now we can easily show this whole sequence of views from anywhere in our app with just a single line of code. If we later decide to tweak the order of the views (maybe we decide to ask for the comment first for some reason) then we don't have to change any of those places in the app that invoke this sequence. We just have to change this one class. Likewise, if we decide that we don't want a modal view and instead we want to reuse an existing NavigationPage then we just touch this one class. That's because all of the navigation calls for this whole sequence (presenting the modal navigation page, pushing views, and popping the modal) are in a single, cohesive class.

Summary

This technique can be used for any self-contained sequence of views within an application, including the app as a whole if you wanted. You can also compose these sequences if needed (that is, one sequence could reuse another sequence as part of its implementation). This is a powerful pattern for keeping code decoupled and cohesive. Anytime you find yourself wanting to put a call to PushAsync or PushModalAsync (or the equivalent on other platforms) within a view itself you should stop and think about how you could restructure that code to keep all of the navigation in one place.


  1. I do not actually recommend using iOS storyboards for multiple reasons, which I may eventually get around to documenting in a blog post. 

22 comments:

  1. Adam..great post.
    Question. I am not using Xamarin Forms. So if I wanted to do something like this I think I have 2 options. 1) pass the calling viewcontroller to my sequencer class so I can call presentviewcontroller within the sequencer (but woudn't this create a string reference to the calling viewcontroller) or 2) have my sequencer class subclass a UInav controller. Thanks
    Bob

    ReplyDelete
  2. Bob, that is a good question. In fact we use this pattern in our non-Xamarin.Forms applications. When we do this we generally pass the presenting view controller as an argument to the Show method, just like in the above example we had to pass the INavigation object as an argument. Passing the view controller by itself does not cause any memory leak issues. It's not a strong reference cycle. It's just a normal reference. There may be other things you can do in your specific view controllers that cause strong reference cycles and make things leak, but just passing in the view controller for presentation is not itself a problem.

    ReplyDelete
  3. Adam..thanks! I implemented IDisposable in my custome sequencer class (just in case)..: )

    ReplyDelete
  4. Using IDisposable doesn't really help here, and it might make the API confusing. This object should be capable of cleaning up after itself when it dismisses. If you use IDisposable then it implies that your client has to call Dispose, but that should be unnecessary. It's just not a clean API. These objects are better when they have very simple APIs that hide all of the details.

    ReplyDelete
  5. I agree so I mod'd it and now it cleans up after itself. Thanks again!

    ReplyDelete
  6. Hi Adam, I'm a bit confused on the commentedImageSequence class you wrote to communicate with the views. If I understand correctly, it is creating not only instances of each class, but also using those pages to subscribe to their respective events? I have a similar Xamarin project that has an event being raised when it's row is selected. When I attempt to respond to that selection and change my second view accordingly I get nothing. I'm looking at your commentImageSequence class, but I can't find my error. Any help will be appreciated!

    ReplyDelete
  7. I'm not sure I understand your problem. The basic idea is that each page should raise an event when something interesting happens, like when a row is selected in your case. It's up to your page class to raise that event at the appropriate time, and it's up to the object that created the page to subscribe to that event and do something. In the CommentImageSequence the CommentImageSequence object subscribes to different events for each page, and in the handler for those events it either pushes a new page or does something else (sets a TaskCompletionSource result in this example, but it could raise an event of its own). If you need more help then you will have to post some code (maybe in GitHub) so I can see what exactly you're doing.

    ReplyDelete
  8. Sure, I have a class A (UI TableView) that is raising an event and I'm trying to subscribe to that event from a segue-connected class B (UI View). I've set my event up to mirror yours as closely as possible, and I have an iPhone alert that's supposed to go off if the event was subscribed to successfully. Because I can't write markdown here, I have a GitHub with two class examples. https://github.com/KNewton009/C-Examples

    Thanks

    ReplyDelete
  9. Your example seems very incomplete so it's difficult to tell exactly what's going on. For instance, in GodController you reference Controller.Instance. What is there? Why are you using a singleton pattern? That certainly doesn't match my example above. Also, GodController is itself a view controller, and it creates another view controller in its ViewDidLoad method. That doesn't match what I have above. View controllers should not directly reference other view controllers. That's the entire point of this blog post. The object that creates the view controllers and subscribes to their events and puts them on the screen should not be a view controller itself. It should just be a simple object that only handles managing the interactions between the view controllers. Try studying my example more closely and then see if you can figure out where you went wrong. If you still can't figure it out then post a complete, compileable/runnable example so that (when I have time) I can try to modify it to fit the pattern.

    ReplyDelete
  10. I apologize, Controller.Instance is referencing a class I didn't have in my example because it did not pertain to my question.
    But, in case it's important, the Controller class was being used to raise a similar event in TableViewController so that GodController could subscribe to that instead. I didn't include that class because it didn't work and wasn't necessary to show the issue I'm having. I should have commented out the subscription to that, but I'm not sure that's the problem.
    GodController is a UI View, yes. Creating the tableView in GodController is where the problem is stemming from. The problem I'm having is that without doing that there's no way for me to subscribe to the event in TableViewController without creating a new TableViewController as I have in my example. I thought I had to have an instance of the event raised in order to have another object subscribed to it, but I may be mistaken.

    I apologize, but I was trying to avoid clogging your blog posts with something that only slightly resembled your example. I didn't want to confuse your readers with information from a different, personal project, example.

    ReplyDelete
  11. I'm having trouble pointing you in the right direction because your example is so incomplete that I can't even tell what you're trying to do. My proposal is that you create a complete working example (including a project and solution that I can build an run), and just code it how you would normally code it. Just make it work so that it demonstrates the UI flow that you're trying to build, and so that I can understand how these UI screens fit together. Then once you've done that I will rewrite it using the pattern above to show you how the pattern is applied to your example.

    ReplyDelete
    Replies
    1. I think the reason the project looks plain is because I'm using Xamarin and it's Storyboards. In order to see the full picture, you would have to have a picture of my Storyboard.
      The Views, and their properties, are being modified from Xamarin's inspectors. Because of the GUI system, I'm connecting them without having to write much code. I'm sorry, I was trying to compact my question as much as possible. I made my GitHub project public, https://github.com/KNewton009/Smite

      I'm trying to make a UIView that has dynamic information. The previous screen is a UITableView with reuseable cells. The user is supposed to click a God and the UIView is supposed to change according to which God the user chose. The alert works and shows me which God the user has selected, but I don't know how to get the information to change accordingly.

      Thank you so much for your time,
      You've given more much than I thought it would have taken.

      Delete
    2. Storyboards are incompatible with this approach. That's one of the problems with storyboards: they lead you to couple views together. This approach is all about decoupling views, and it does that by putting all of the navigation logic in a controller object (in code). Storyboards give you a visual representation of a navigation flow, but they usually require code to support that flow to be spread out amongst the view controllers involved in the flow. For instance, when you use a segue to navigate from one view controller to another you typically need to configure the destination view controller before it's presented, and you do that by overriding the PrepareForSegue method in the source view controller. That couples the source view controller to the destination view controller. That's what this post is trying to avoid.

      If you want to use this approach you should throw away your storyboard. You have to choose between decoupling views or using storyboards. You can't have both.

      Delete
    3. Ok, I understand. Because of the storyboard, maybe it's an updated feature(?), I never had to override PrepareForSegue. I never realized the storyboards were making all of the views dependent. My problem still is that I never call new on any objects because they're called via the storyboard method so I can't subscribe to the event being called.

      Thanks for your time, I'll try another method.

      Delete
    4. "My problem still is that I never call new on any objects because they're called via the storyboard method so I can't subscribe to the event being called."

      That's exactly the kind of thing that PrepareForSegue is for. It's for doing additional setup on the destination view controller before you transition to it.

      The only way you could get away from having to implement PrepareForSegue is if your destination view controller is always exactly the same regardless of what was selected. If you want to do something like select an item from a table view and then show a new view controller with more details about that item then you would have to use PrepareForSegue in order to give the new view controller information about which object was just selected. That's where the coupling comes in.

      I would guess that either you've only had trivial use cases for storyboards so far or you're doing something very strange as a result of not understanding how storyboards are supposed to work. Maybe this is where your singleton came into play?

      Delete
    5. Thanks, PrepareForSegue was exactly what I was looking for. All of the tutorials and examples I was reading about the different views and overrides didn't point me in that direction. I can finally complete my project.
      I don't specialize in developing mobile applications so methods like PrepareForSegue aren't instantly familiar. As far as I know, PrepareForSegue is exclusive to iOS devices. I'm using storyboards as a method to further my knowledge as a programmer. I wouldn't label anything that helps that as trivial.

      Again thank you for you help.

      Delete
    6. Sorry, I wasn't saying that storyboards are trivial. I was saying that the things you have implemented so far using storyboards must have been very simple use cases, and I say that because it is very unusual to not have any uses of PrepareForSegue. I wasn't trying to criticize at all. I was just trying to explain how it could be possible that you never used that function.

      Feel free to use whatever tool you find most useful, but if you choose to use storyboards then the pattern described in this blog post is not applicable.

      Delete
    7. I understand, thanks very much for pointing me in the right direction, I have to dig a bit deeper into the API and learn some more syntax. Understanding PrepareForSegue also made this post a lot clearer.

      Delete
  12. Adam, nice post. I'm wondering what if you wanted to have 2 levels of cancel logic. For example, what if you wanted the cancel on the take new image page to just drop the user back on the image picker page rather than cancelling the entire sequence (like the cancel button on the image picker or comment page would)?

    ReplyDelete
    Replies
    1. In that case you would just have different cancel buttons with different actions instead of having a single shared cancel button.

      Delete
  13. Great Post. I already forgot too much of the patterns I learned at university. This will make my Xamarin code much better. I am Looking forward to change my navigation model tomorrow. Thanks a lot for that!

    ReplyDelete