Creating a Xamarin Forms App Part 9 : Working with Alerts and Dialogs

  • Part 1 : Introduction
  • Part 2 : Getting Started
  • Part 3 : How to use Xamarin Forms with Visual Studio without the Business Edition
  • Part 4 : Application Resources
  • Part 5 : Dependency Injection
  • Part 6 : View Model First Navigation
  • Part 7 : Unit Testing
  • Part 8 : Consuming a RESTful Web Service
  • Part 9 : Working with Alerts and Dialogs
  • Part 10 : Designing and Developing the User Interface
  • Part 11 : Updating to Xamarin Forms 1.3
  • Part 12 : Extending the User Interface

In my last post I showed how to consume a RESTful web service to display data in my App. In this post I am going to show you how to handle exceptions and display an appropriate message when something goes wrong or when the web service is not available.

Xamarin Forms provides the DisplayAlert and DisplayActionSheet methods on the Page class to show dialog messages to the user. I don’t want to work directly with the Page class in my service. What I need is a service that I can inject that will provide methods for showing alert messages.

I am going to create IDialogProvider in Core Interfaces which exposes the alert methods defined on the page class.

namespace Silkweb.Mobile.Core.Interfaces
{
    public interface IDialogProvider
    {
        Task DisplayAlert(string title, string message, string cancel);

        Task<bool> DisplayAlert(string title, string message, string accept, string cancel);

        Task<string> DisplayActionSheet(string title, string cancel, string destruction, params string[] buttons);
    }
}

The implementation needs to call the current page that will display the alert. This however would prove difficult to test because the page will show a dialog and won’t return a result until the user closes the dialog. So I am going to create an abstraction of Page called IPage. This will inherit from IDialogProvider and also expose the Page Navigation property, which I can use in the Navigator class.

namespace Silkweb.Mobile.Core.Interfaces
{
    public interface IPage : IDialogProvider
    {
        INavigation Navigation { get; }
    }
}

You may be thinking that this is just creating another abstraction and merely moving the problem. And you’d be right. However, because I am moving Page to another class it contains the testability problem and means that the rest of my application becomes much more testable.

I will implement this as a PageProxy that simply wraps the Page class. I have placed this in Core Views as essentially it is wrapping a View. I also need to resolve the current page so that we can display alerts on the currently displayed page otherwise alerts won’t appear. We can do this using Func<Page> which acts as a resolver function for the current page. This I can inject in to the PageProxy in the constructor. Here is the PageProxy class.

namespace Silkweb.Mobile.Core.Views
{
    public class PageProxy : IPage
    {
        private readonly Func<Page> _pageResolver;

        public PageProxy(Func<Page> pageResolver)
        {
            _pageResolver = pageResolver;
        }

        public async Task DisplayAlert(string title, string message, string cancel)
        {
            await _pageResolver().DisplayAlert(title, message, cancel);
        }

        public async Task<bool> DisplayAlert(string title, string message, string accept, string cancel)
        {
            return await _pageResolver().DisplayAlert(title, message, accept, cancel);
        }

        public async Task<string> DisplayActionSheet(string title, string cancel, string destruction, params string[] buttons)
        {
            return await _pageResolver().DisplayActionSheet(title, cancel, destruction, buttons);
        }

        public INavigation Navigation
        {
            get { return _pageResolver().Navigation; }
        }
    }
}

Resolving the current page isn’t as straightforward as you might think, as there are a number of scenarios that might exist. The Main Page could be a MasterDetailPage, in which case we need to get the detail page for this, or it might be a NavigationPage so we would need the current page for that. Or it might even just be a single page that we could just return. Also the detail page of a MasterDetailPage might itself be a NavigationPage, in which case we would need the current page of the detail page. I have provided a default page resolver that encapsulates this logic, which I register in the Core Autofac Module like this.

builder.RegisterInstance<Func<Page>>(() =>
            {
                // Check if we are using MasterDetailPage
                var masterDetailPage = App.Current.MainPage as MasterDetailPage;

                var page = masterDetailPage != null 
                    ? masterDetailPage.Detail 
                    : App.Current.MainPage;

                // Check if page is a NavigationPage
                var navigationPage = page as IPageContainer<Page>;

                return navigationPage != null 
                    ? navigationPage.CurrentPage
                    : page;
            }
);

We can override this with a simpler resolver if we know what our page is going to be. I know that this App is using NavigatorPage as the main page therefore I just need to add this registration in the shared PCL of my App which will override the one above.

builder.RegisterInstance<Func<Page>>(() => ((NavigationPage)App.Current.MainPage).CurrentPage);

Now lets implement the DialogService where we inject an instance of IPage.

namespace Silkweb.Mobile.Core.Services
{
    public class DialogService : IDialogProvider
    {
        private readonly IPage _page;

        public DialogService(IPage page)
        {
            _page = page;            
        }

        public Task DisplayAlert( string title, string message, string cancel)
        {
            return _page.DisplayAlert(title, message, cancel);
        }

        public async Task<bool> DisplayAlert(string title, string message, string accept, string cancel)
        {
            return await _page.DisplayAlert(title, message, accept, cancel);
        }

        public async Task<string> DisplayActionSheet(string title, string cancel, string destruction, params string[] buttons)
        {
            return await _page.DisplayActionSheet(title, cancel, destruction, buttons);
        }
    }
}

This now makes this easily testable. Lets create our test fixture and write some tests for each method. I have created the following test fixture in the Core Tests project.

namespace Silkweb.Mobile.Core.Tests.Services
{
    [TestFixture]
    public class DialogServiceFixture
    {
        [Test]
        public void DisplaysAlert()
        {
            var page = new Mock<IPage>();
            var dialogService = new DialogService(page.Object);

            dialogService.DisplayAlert ("Alert", "You have been alerted", "OK");
            page.Verify(x => x.DisplayAlert("Alert", "You have been alerted", "OK"));
        }

        [Test]
        public async void DisplaysAlertWithResponse()
        {
            var page = new Mock<IPage>();
            page.Setup(x => x.DisplayAlert(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
                .ReturnsAsync(true);

            var dialogService = new DialogService(page.Object);

            var answer = await dialogService.DisplayAlert("Question?", "Would you like to play a game", "Yes", "No");

            page.Verify(x => x.DisplayAlert("Question?", "Would you like to play a game", "Yes", "No"));
            Assert.That(answer, Is.True);
        }

        [Test]
        public async void DisplaysActionSheetWithResponse()
        {
            var page = new Mock<IPage>();
            page.Setup(x => x.DisplayActionSheet("ActionSheet: Send to?", "Cancel", null, "Email", "Twitter", "Facebook"))
                .ReturnsAsync("Yes");

            var dialogService = new DialogService(page.Object);

            var answer = await dialogService.DisplayActionSheet("ActionSheet: Send to?", "Cancel", null, "Email", "Twitter", "Facebook");

            page.Verify(x => x.DisplayActionSheet("ActionSheet: Send to?", "Cancel", null, "Email", "Twitter", "Facebook"));
            Assert.That(answer, Is.EqualTo("Yes"));
        }

    }
}

Notice that for each test I am mocking IPage using Moq and verifying the alert methods are called using Verify. If we were using Page directly instead of IPage then the calls to DisplayAlert and DisplayActionSheet would be very difficult to test. Lets run these tests.

1

Yay, they all pass.

Now I need to bootstrap both IDialogProvider and the PageProxy in the Core Bootstrapping Autofac Module.

namespace Silkweb.Mobile.Core.Bootstrapping
{
    public class AutofacModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            // service registration
            builder.RegisterType<DialogService>()
                .As<IDialogProvider>()
                .SingleInstance();



            // Current PageProxy
            builder.RegisterType<PageProxy>()
                .As<IPage>()
                .SingleInstance();
        }
    }
}

Now I need to add exception handling to the MountainWeatherService. For brevity I will just show you the GetAreas Method.

        public async Task<IEnumerable<Location>>  GetAreas()
        {
            string result;

            try
            {
                Debug.WriteLine("Getting Locations from DataPoint Service...");
                result = await Get("txt/wxfcs/mountainarea/json/sitelist");   
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to get locations from service provider. The service maybe down. Retry or try again later.", ex);
            }

            Location[] locations;

            try
            {                
                var locationToken = JObject.Parse(result)["Locations"]["Location"];

                locations = locationToken.Select(location => new Location()
                    { 
                        Id = (int)location["@id"], 
                        Name = (string)location["@name"] 
                    }).ToArray();
            }
            catch (Exception ex)
            {
                throw new Exception("Failed to format the mountain areas site list.", ex);
            }

            return locations;
        }

I am handling two cases here. The first handles exceptions when making the the web service call and the other when parsing the results. I could also to check whether the device has an online connection and provide a timeout, but for now I want to keep things simple.

Let’s now use IDialogProvider to display alerts when the exceptions get thrown. I simply need to inject this in to the view model, provide some exception handling around the service calls and display an appropriate alert if something goes wrong. Here is the updated SetAreas method in the MountainAreasViewModel

        private async void SetAreas()
        {
            try
            {
                var locations = await _mountainWeatherService.GetAreas();

                if (locations == null)
                    return;

                Areas = locations
                    .Select(location =>  _areaViewModelFactory(location))
                    .ToList();
            }
            catch (Exception ex)
            {
                var result = await _dialogProvider.DisplayActionSheet(ex.Message, "Cancel", null, "Retry");

                if (result == "Retry")
                    SetAreas();
            }
        }

This catches exceptions and displays an Action Sheet to give the user the opportunity to retry. Here’s the updated ShowForecast method in the MountainAreaViewModel where we handle exceptions in the same way.

private async void ShowForecast()
        {
            try
            {
                ForecastReport forecastReport = await _mountainWeatherService.GetAreaForecast(_location.Id);

                if (forecastReport == null)
                    return;

                await _navigator.PushAsync<ForecastReportViewModel>(vm => 
                    {
                        vm.Title = _location.Name;
                        vm.ForecastReport = forecastReport;
                    }
                );
            }
            catch (Exception ex)
            {
                var result = await _dialogProvider.DisplayActionSheet(ex.Message, "Cancel", null, "Retry");

                if (result == "Retry")
                    ShowForecast();
            }
        } 

Now lets run the App disconnected from the Internet in the iOS simulator.

2

The action sheet gets displayed with a message and options to Retry or Cancel. Clicking/Tapping on Retry causes the App to retry the request. Let’s re-connect to the Internet and select Retry.

3
Now the service succeeds and the mountain areas list gets displayed.

Finally I also exposed the Navigator property on IPage. I now need to update the Navigator class by injecting the Func<IPage> page resolver and use the Navigator property, which I added to IPage earlier. This will make the Navigator class much more testable.

namespace Silkweb.Mobile.Core.Services
{
    public class Navigator : INavigator
    {
        private readonly IPage _page;
        private readonly IViewFactory _viewFactory;

        public Navigator(IPage page, IViewFactory viewFactory)
        {
            _page = page;
            _viewFactory = viewFactory;
        }

        private INavigation Navigation
        {
            get { return _page.Navigation; }
        }

        public async Task<IViewModel> PopAsync()
        {
            Page view = await Navigation.PopAsync();
            return view.BindingContext as IViewModel;
        }

        
    }
}

Func<IPage> gets injected and then I access the page navigation using the Navigation property. This guarantees that I will always be navigating using the INavigation of the current page.

Let’s now update the Navigator unit test fixture.

namespace Silkweb.Mobile.Core.Tests.Services
{
    [TestFixture]
    public class NavigatorFixture
    {
        private MockViewModel _viewModel;
        private Navigator _navigator;
        private Action<MockViewModel> _action;

        [SetUp]
        public void SetUp()
        {
            _action = x => x.Title = "Test";
            _viewModel = new MockViewModel();
            var navigation = new Mock<INavigation>();
            var page = new Mock<IPage>();

            page.Setup(x => x.Navigation).Returns(navigation.Object);

            navigation.Setup(x => x.PopAsync()).ReturnsAsync(new Page { BindingContext = new MockViewModel()});
            navigation.Setup(x => x.PopModalAsync()).ReturnsAsync(new Page { BindingContext = new MockViewModel()});
            navigation.Setup(x => x.PopToRootAsync());

            var viewFactory = new Mock<IViewFactory>();
            viewFactory.Setup(x => x.Resolve<MockViewModel>(out _viewModel, _action)).Returns(new MockView());

            _navigator = new Navigator(page.Object, viewFactory.Object);
        }

        [Test]
        public async void NavigateToView()
        {
            MockViewModel viewModel = await _navigator.PushAsync<MockViewModel>(_action);
            Assert.That(viewModel, Is.EqualTo(_viewModel));
        }

         {Some tests excluded for brevity}
    }
}

Notice here that I can now simply Mock IPage which makes the Navigator class much more testable. Let’s run this text fixture.

4

Yay, they all pass.

In this post I have showed you how you can display alerts in your application in a testable manner by implementing a DialogService. I have also showed how we can abstract away the Page class to aid the testability of our application. I have also added exception handling to a RESTful service and used the DialogService to handle exceptions and display an action sheet to the user so they can take appropriate action.

15 thoughts on “Creating a Xamarin Forms App Part 9 : Working with Alerts and Dialogs

  1. Hi,
    I really enjoy your series. I’m actually using it to learn X.F.

    One question:
    You’re using C# 6.0 here. I can’t make this working in VS 2013/4, because the required Roslyn NuGet packages are not compatible with PCLs targeting Xamarin stuff.

    Am I missing something here or do you use a different environment?

    Regards
    Thomas

    Like

    1. Hi,

      I’m pleased to hear you like my series and you are actually using it to learn Xamarn Forms.

      I’m not sure if I’m following what you are saying but I think my latest source code on GitHub is using Xamarin 1.3.1 to target Unified API. I think this requires that you are running on the Beta Channel of Xamarin. Open Xamarin Studio, go to updates and change to the Beta channel.

      Jonathan

      Like

      1. Hi,

        I was referring to the fact that you are using ‘await’ within a ‘catch’ block – which is not allowed prior to C# 6.0. And C# 6.0 requires the new Roslyn compiler which comes with VS 2015.
        Although Roslyn is available as NuGet package, it is not compatible with PCLs targeting Xamarin. As a result, you cannot use it with Xamarin in VS 2013 Upd. 4.

        But in the meantime I have seen that the respective code (calling ‘DisplayAlert’ in a catch block) in your GitHub repo is slightly different from the sample code here so that it works also with ‘traditional’ C#. This consequently answered my question.

        Regards
        Thomas

        Like

      2. Hi,

        I think I see what you are referring to now. I’m not using anything more than what is available with Xamarin Forms 1.3. I Beleive the code you are referring to is in the MountainAreasViewModel.

        catch (Exception ex)
        {
        Action action = async () =>
        {
        var result = await _dialogProvider.DisplayActionSheet(ex.Message, “Cancel”, null, “Retry”);

        if (result == “Retry”)
        SetAreas();
        };

        action();
        }

        This isn’t actually using aync directly in the catch, because as you pointed out you can not do that. As a workaround the suggested solution is to wrap the async call in an action and then execute the action as shown above. This works fine with Xamarin Forms without the need for C# 6 or Roslyn compiler.

        Hope this helps.

        Jonathan

        Like

  2. Yep, that’s what I saw when I looked it up in your GitHub repo.

    The reason why I was confused at first is that the respective code version here in your blog is slightly different:

    catch (Exception ex)
    {
    var result = await _dialogProvider.DisplayActionSheet(ex.Message, “Cancel”, null, “Retry”);

    if (result == “Retry”)
    ShowForecast()
    }

    And this version wouldn’t work in VS 2013. Anyway, thanks for your reply.

    Regards
    Thomas

    Like

  3. Jonathan,

    I’m attempting to use your framework underneath an app with a MasterDetailPage as the base page. That is to say, I have a MasterDetailPage with (currently 3) different page options in a ListView as the Master, which when elected should navigate the Detail to another view. The issue I am having is that that the command method (included below) does indeed trigger, but does not change any of the displayed views. Any suggestions you have would be much appreciated.

    private void NavigateDetailPage()
    {
    switch (_option) {
    case “Options”:
    //_navigator.PushAsync ();
    Debug.WriteLine(“Options”);
    break;

    case “My Recipes”:
    _navigator.PushAsync();
    Debug.WriteLine(“My Recipes”);
    break;

    case “Home”:
    _navigator.PushAsync();
    Debug.WriteLine(“Home”);
    break;
    }
    }

    Like

    1. Jonathan,
      Not sure if you’ve seen this, but I have an update. I’ve solved the navigation problem as follows:
      private void NavigateDetailPage()
      {
      switch (_option) {
      case “Options”:
      //_navigator.PushAsync ();
      Debug.WriteLine(“Options”);
      break;

      case “My Recipes”:
      ((MasterDetailPage)_viewFactory.Resolve()).Detail = new NavigationPage(_viewFactory.Resolve());
      Debug.WriteLine(“My Recipes”);
      break;

      case “Home”:
      ((MasterDetailPage)_viewFactory.Resolve()).Detail = new NavigationPage(_viewFactory.Resolve());
      Debug.WriteLine(“Home”);
      break;
      }
      }

      That is to say, I resolve the masterdetailpage serving as my main page (registered as single instance, of course), and change the detail in this method. If you get around to seeing this, let me know your thoughts on this solution!

      Like

      1. Shane,

        That is one approach. I’m not so sure about the switch statement though.

        I think the correct approach is to create you mainpage as MasterDetail (as you perhaps have) in the bootstrapper like this:

        var masterDetail = new MasterDetailPage();
        masterDetail.Detail = new NavigationPage();
        _application.MainPage = masterDetail;

        and then register IPage in your module like this:

        builder.RegisterInstance<Func>(() =>
        ((MasterDetailPage)Application.Current.MainPage).Detail);

        this will then get injected in to the PageProxy class which is registered as IPage and gets injected into the Navigator. So when you call navigator.PushAsunc it will use the navigation page set as you Detail page.

        Like

  4. hi Jonathan

    Great series of articles, they have been a great source in my initiation on the xamarin platform, keep up the good work!!!

    i have one question, right now i have one issue with my solution (I’ve been making the same app as you) and on the MountainAreasViewModel i get a nullpointerexception when trying to display an Alert, this is because the page resolver of the PageProxy resolves to null and that is kind of logical because the main page of the app get sets after the MountainAreasViewModel get resolves.

    in the bootstrapper

    var mainPage = viewFactory.Resolve (); — at this point theres no main page set
    …….
    _app.MainPage = navigationPage;

    its my line of thinking right, and if so how do i show an alert on the constructor of the MountainAreasViewModel

    Like

    1. Hi, Sorry for the late reply.

      I guess you would need to create the NavigationPage first and then the Viewmodel for the root page afterwards. You could create your main page, then create and set the NavigationPage and set the app.MainPage and then create the ViewModel and set the BindingContext on your main Page afterwards. That way the app.MainPage will then exist. Be interesting to hear to you resolve this.

      Like

  5. Instead of creating an IDialogProvider service, what would you think of adding a property to the ViewModelBase that looks like this:

    public Exception Fault {
    get { return _fault; }
    set { SetProperty(ref _fault, value); }
    }

    With this, would it be possible to notify the view (or page) of the exception? In other words, perhaps the XAML code-behind could have a callback that is signaled through the PropertyChangedEventHandler, which can then create the alert dialogue?

    I ask because it feels like the view-model is taking on too much responsibility when it is given an instance of IDialogProvider. Signalling the view through the PropertyChangedEventHandler is a pre-existing communication path. It would be very cool if errors were also signaled that way.

    Like

  6. Hi Jonathan,

    Nice solution. However, Prism offers this out of the box with the IPageDialogService. This is one of the reasons I’m glad we choose for Prism.
    I saw your discussion with Brian Lagunas about ViewModel first navigation. In the last version of Prism for Xamarin.Forms it is possible to navigate from ViewModel to ViewModel. I think it is awesome! Maybe you should reconsider using Prism?

    Cheers!

    Like

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 )

Google+ photo

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

Connecting to %s