22 December 2014

A behavior to show a MessageDialog from a MVVMLight viewmodel in Universal apps–with callbacks

Of course you are now all writing MVVM based apps, after all I have been advocating that for years, and you are listening this here old venerable MVP, right? ;). Anyway, some UI interactions between UI and viewmodel can seem to be a bit complicated, but these kinds of problems are usually pretty simple to solve with a combination of a behavior, the MVVMLight messenger, and a message containing a callback. And as I hate to write things more than once, I  made this into a reusable behavior. It ain’t rocket science, but it’s nice and clean.

I start with a class holding the actual message, and in this class file I have defined a callback delegate as well:

using System.Threading.Tasks;
using GalaSoft.MvvmLight.Messaging;

namespace WpWinNl.Behaviors
{
  public class MessageDialogMessage : MessageBase
  {
    public MessageDialogMessage(string message, string title,
                                string okText, string cancelText,
                                MessageDialogCallBack okCallback = null,
                                MessageDialogCallBack cancelCallback = null,
                                object sender = null, object target = null)
                                : base(sender, target)
    {
      Message = message;
      Title = title;
      OkText = okText;
      CancelText = cancelText;
      OkCallback = okCallback;
      CancelCallback = cancelCallback;
    }

    public MessageDialogCallBack OkCallback { get; private set; }
    public MessageDialogCallBack CancelCallback { get; private set; }
    public string Title { get; private set; }
    public string Message { get; private set; }
    public string OkText { get; private set; }
    public string CancelText { get; private set; }
  }

  public delegate Task MessageDialogCallBack();
}

It’s a basic MVVMLight BaseMessage child class, with properties for various texts, and two optional callbacks for OK and cancel actions.

The behavior itself is, as behaviors go, pretty simple:

using System;
using System.Diagnostics;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using GalaSoft.MvvmLight.Messaging;

namespace WpWinNl.Behaviors
{
  public class MessageDialogBehavior : SafeBehavior<FrameworkElement>
  {
    protected override void OnSetup()
    {
      Messenger.Default.Register<MessageDialogMessage>(this, ProcessMessage);
      base.OnSetup();
    }

    private async void ProcessMessage(MessageDialogMessage m)
    {
      bool result = false;
      var dialog = new MessageDialog(m.Message, m.Title);

      if (!string.IsNullOrWhiteSpace(m.OkText))
      {
        dialog.Commands.Add(new UICommand(m.OkText, cmd => result = true));
      }

      if (!string.IsNullOrWhiteSpace(m.CancelText))
      {
        dialog.Commands.Add(new UICommand(m.CancelText, cmd => result = false));
      }

      try
      {
        await dialog.ShowAsync();
        if (result && m.OkCallback != null)
        {
          await m.OkCallback();
        }

        if (!result && m.CancelCallback != null)
        {
          await m.CancelCallback();
        }
      }
      catch (Exception ex)
      {
        Debug.WriteLine("double call - ain't going to work");
      }
    }
  }
}

This being once again a SafeBehavior, it simply starts to listen to MessageDialogMessage messages. When one is received, a MessageDialog is constructed with the message and title to display, as well as an optional Ok and Cancelbutton – and callback. If you send a message, you can provide optionally provide simple method from your viewmodel to be called by the behavior when Ok or Cancel is called, thus providing a two-way communication channel. Mind, though, all this is async, which has a curious side effect: if you send another message before the user has clicked Ok or Cancel, this will raise an exception.This will be displayed in the Visual Studio output window while you debug, but it won’t show up in release, and your message won’t be displayed either – so you need to carefully control sending messages, and not throw them out like there’s no tomorrow.

I have made a little sample app – a Universal app of course, because there have been some pretty clear hints by Microsoft this is the way to go forward. You will notice everything worthwhile is the shared project, up to the MainPage.xaml. People who were present at the latest Lowlands WpDev day last October will remember my fellow MVP Nico Vermeir saying in his talk “don’t do that, there’s pain in there”, and while this is true, it’s an ideal thing for lazy demo writers.

I started out with a blank Universal app, and moved the MainPage.Xaml and it’s companion MainPage.Xaml.cs to the shared project. Then I brought in my WpWinNl project that, amongst other things, drags in MVVMLight and everything else you need. Then I created this whopping complex viewmodel:

using System.Threading.Tasks;
using System.Windows.Input;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;
using WpWinNl.Behaviors;

namespace SampleMessagePopup
{
  public class MainViewModel : ViewModelBase
  {
    public ICommand MessageCommand
    {
      get
      {
        return new RelayCommand(
          () => 
            Messenger.Default.Send(new MessageDialogMessage(
              "Do you really want to do this?", "My Title", 
              "Hell yeah!", "No way", 
              HellYeah, Nope)));
      }
    }

    private async Task HellYeah()
    {
      Result = "Hell yeah!";
    }

    private async Task Nope()
    {
      Result = "NOOOO!";
    }


    private string result = "what?";
    public string Result
    {
      get { return result; }
      set { Set(() => Result, ref result, value); }
    }
  }
}

imageimageIf you fire the command, it will show a dialog that looks like the left on these two screens (on Windows Phone) and if you press the “no way” button it will show the very right screen. A few things are noticeable:

  • Both methods are async, although they don’t implement anything asynchronous. That’s because I wanted to be able do awaitable things in the callback and a contract is a contract – were not making JavaScript here. Only tools like ReSharper (highly recommended) will make you notice it, but you can ignore it for now.
  • You might notice the callback methods are private but still the behavior is apparently able to call these methods. This, my dear friends who grew up in the nice era of managed OO languages that shield you from the things that move below you in the dark dungeons of your processor, is because you basically give a pointer to a method – and that’s accessible anywhere. Your compiler may tell you it’s a private method, but that’s more like it has a secret telephone number – if you got that, you can access it anywhere ;-). Us oldies who grew up with C or Assembler can tell you all about the times when boats were made of wood and men were made of steel and the great ways you can shoot yourself in the foot with this kind of things. If you want to make you app testable it makes more sense to make the methods public, by the way, but I could not let the opportunity to show this great example of the law of leaky abstractions pass.

But I digress, I know, that is also a sign of age ;)

The Xaml is of course also pretty simple – as this app does not do much:

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Interactivity="using:Microsoft.Xaml.Interactivity" 
    xmlns:Behaviors="using:WpWinNl.Behaviors"
    x:Class="SampleMessagePopup.MainPage"
    DataContext="{StaticResource MainViewModel}">
  <Interactivity:Interaction.Behaviors>
    <Behaviors:MessageDialogBehavior/>
  </Interactivity:Interaction.Behaviors>
  <Page.BottomAppBar>
    <CommandBar>
      <AppBarButton Icon="Accept" Label="go ask!" 
                    Command="{Binding MessageCommand}"/>
    </CommandBar>
  </Page.BottomAppBar>

  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" >
  	<TextBlock Text="{Binding Result}" HorizontalAlignment="Center" 
                   VerticalAlignment="Center" FontSize="56"/>
  </Grid>
</Page>

Notice the behavior sitting on the page itself. For obvious reasons, it’s the logical place to put. You can also put it on the top grid. But whatever you do, use only one per page. They all listen to the same message, and if you put more of them on one page you will get an interesting situation where multiple behaviors will try to show the same MessageDialog. I have not tried this, but I assume it won’t be very useful.

To make this work, by the way, you need of course to declare your viewmodel in App.Xaml. You have no other choice here, as the App Bar sits at the top level of the page, the button calls the command and thus the resource must be one level higher, i.e. the app itself.

<Application
    x:Class="SampleMessagePopup.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:SampleMessagePopup"
    xmlns:system="using:System">

  <Application.Resources>
    <local:MainViewModel x:Key="MainViewModel" />
  </Application.Resources>
</Application>

Again, if you use something like ReSharper a simple click on the declaration in MainPage.Xaml will make this happen.

So, that’s all there is to it. You won’t find the code for this behavior in the solution, as it is already in WpWinNl. Just like the previous one ;). As usual, a sample solution is provided, but in inspired by my fellow MVP Diederik Krols I’ve have decided that going forward I will publish those on GitHub.

13 December 2014

A behavior to deal with UI consequences of full screen and Software Keys in Windows Phone 8.1

This week I was presented with an interesting challenge. Using this technique, I used the whole screen for my app. The problem was I had not anticipated a Denim feature for the so called software buttons. For those unfamiliar with that – on the newest low and mid-tier phones the back, start and search buttons are not necessarily hardware buttons anymore, but can be a dedicated piece of screen that shows buttons. This enables hardware manufacturers to make phones for all platforms in one (hardware) package. Now the Denim firmware – that comes with the Lumia 73x and 83x - enables users to make those software buttons disappear – so the extra piece of screen can be used by the app itself. Pretty awesome. This can be done by using pressing a little arrow that appears on the left of the button bar:image


It can be brought up again by swiping in from the bottom of the imagescreen. Pretty cool, but with a side effect I had not anticipated. If the application view bound mode is set to ApplicationViewBoundsMode.UseCoreWindow in App.Xaml.cs the phone reports the whole screen size – not only the part that is normally taken by the status bar on top and the application bar at the bottom, but also the part that is used by the button bar. The My SensorCore app Travalyzer employs slide-in ‘windows’ that slide in from the side and stick to the bottom. I just took an offset the size of the application bar, and I was done, right? Yeah. Until my app got deployed to Denim phones. When the button bar is hidden, there is no problem as you can see to the right. But when it is not hidden… image

I believe the correct word in this circumstance is something like “blimey”, or maybe “crikey”, depending on what kind part of the globe you come from.

The solution is – you guessed it – a behavior. Or actually, two. But one is an oldie. Never one for original names, I have called it KeepFromBottomBehavior.

The code is actually surprisingly simple:
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;

namespace WpWinNl.Behaviors
{
  public class KeepFromBottomBehavior : SafeBehavior<FrameworkElement>
  {
    private double originalBottomMargin;
    protected override void OnSetup()
    {
      originalBottomMargin = AssociatedObject.Margin.Bottom;
      UpdateBottomMargin();
      ApplicationView.GetForCurrentView().VisibleBoundsChanged += 
        KeepInViewBehaviorVisibleBoundsChanged;

      base.OnSetup();
    }

    void KeepInViewBehaviorVisibleBoundsChanged(ApplicationView sender, object args)
    {
      UpdateBottomMargin();
    }

    private void UpdateBottomMargin()
    {
      if (WindowHeight > 0.01)
      {
        var currentMargins = AssociatedObject.Margin;

        var newMargin = new Thickness(
          currentMargins.Left, currentMargins.Top, currentMargins.Right,
          originalBottomMargin + 
            (WindowHeight - ApplicationView.GetForCurrentView().VisibleBounds.Bottom));
        AssociatedObject.Margin = newMargin;
      }
    }

    #region WindowHeight
  }
}

Like all my behaviors, it’s a SafeBehavior so you have a nice and easy base class. It first saves the current Bottom margin, and then calls the “UpdateBottomMargin” method. That method assumes “WindowHeight” contains the actual (full) height of the space available to the app. It subtracts that from that height the bottom of the rectangle depicting the visible bounds, that is – the part that is, according to the phone, not obscured by an App Bar. That it adds to the original bottom margin (in my app that is zero – I want the window to stick to the very bottom). Net effect: the object to which this behavior is attached always moves upward and downward if the user opens or closes the ‘software button bar’, and if he rotates the phone, it takes that into account as well.

Now WindowHeight is a region (yeah flame me, I use that for Dependency properties) containing a  Dependency property that calls UpdateBottomMargin as well if the WindowHeight changes.

public const string WindowHeightPropertyName = "WindowHeight";

public double WindowHeight
{
  get { return (double)GetValue(WindowHeightProperty); }
  set { SetValue(WindowHeightProperty, value); }
}

public static readonly DependencyProperty WindowHeightProperty = DependencyProperty.Register(
    WindowHeightPropertyName,
    typeof(double),
    typeof(KeepFromBottomBehavior),
    new PropertyMetadata(default(double), WindowHeightChanged));
 
public static void WindowHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
  var thisobj = d as KeepFromBottomBehavior;
  var newValue = (double)e.NewValue;
  if (thisobj != null)
  {
    thisobj.UpdateBottomMargin();
  }
}

But how does this property get it’s value? Enter our old friend SizeListenerBehavior. How this all works, will be demonstrated using a simple app. First,we need to have an emulator capable of displaying software keys. The 8.1 U1 WXGA 4.5 inch fits the bill.To enable soft keys display, you will need to open the additional tools, and under sensor you will find the “Software buttons” checkbox. The emulator will reboot, and then show a screen with software keysimageimageEven with just hardware keys most of the ‘popup’ already disappears behind the app bar, but if you hide the software keys, then swipe them up again, it indeed looks pretty bad – the text “Some popup” has disappeared behind the software buttons bar, and most of the controls that could be there are hardly readable, let alone usable to the user.

The XAML for this page is not quite the most complex in the world.
<Page [name space stuff omitted]
    >
  <Page.BottomAppBar>
    <CommandBar Opacity="0.7">
      <AppBarButton Icon="Accept" Label="Click"/>
    </CommandBar>
  </Page.BottomAppBar>

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <!-- Title Panel -->
    <StackPanel Grid.Row="0" Margin="12,0,0,0">
      <TextBlock  Text="MY APP" Style="{ThemeResource TitleTextBlockStyle}" 
                  Margin="0,12,0,0" />
      <TextBlock Text="a map" Margin="0,-6.5,0,26.5" 
                 Style="{ThemeResource HeaderTextBlockStyle}" 
                 CharacterSpacing="{ThemeResource PivotHeaderItemCharacterSpacing}" 
                 VerticalAlignment="Top"/>
    </StackPanel>

    <Maps:MapControl Grid.Row="1"/>
    <Grid Height="150" Grid.Row="1" VerticalAlignment="Bottom" 
      Background="#FF7A2222" >
      <TextBlock Text="Some popup"  
                 Style="{ThemeResource TitleTextBlockStyle}" 
                 VerticalAlignment="Bottom" HorizontalAlignment="Center" />
    </Grid>
  </Grid>
</Page>

Now to solve the problem, follow just these easy steps:

  1. Pull in the newest version of WpWinNl from NuGet. Make sure you have set the NuGet package manager settings to “including prerelease’. You will need to have the 2.1.2 alpha version or higher. Don’t worry about that alpha – I am already using this in my apps, I just haven’t got around to making this a final version.
  2. Compile the application
  3. Open the Windows Phone project in Blend
  4. Find the SizeListenerBehavior in “Assets” , drag it on top of the Page Element, rename it to ContentRootListener
    image
  5. Find the “KeepFromBottomBehavior”, then drag it on top of the grid holding the ‘popup’
    image
  6. On the right hand side, find the “Properties” tab and select the little square beside “WindowHeight”
    image
    In the popup menu, select “Bind to Element”
  7. Now select ContentRootListener element again (in the Objects and TimeLine tab where you just put it in step 4
  8. Select WatchedObjectHeight. That’s it. You are done.
The XAML now looks like this:
<Page [name space stuff omitted]
   >
  <Page.BottomAppBar>
    <CommandBar Opacity="0.7">
      <AppBarButton Icon="Accept" Label="Click"/>
    </CommandBar>
  </Page.BottomAppBar>

  <Interactivity:Interaction.Behaviors>
    <Behaviors:SizeListenerBehavior x:Name="ContentRootListener"/>
  </Interactivity:Interaction.Behaviors>

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <!-- Title Panel -->
    <StackPanel Grid.Row="0" Margin="12,0,0,0">
      <TextBlock  Text="MY APP" Style="{ThemeResource TitleTextBlockStyle}" 
        Margin="0,12,0,0" />
      <TextBlock Text="a map" Margin="0,-6.5,0,26.5" 
        Style="{ThemeResource HeaderTextBlockStyle}" 
        CharacterSpacing="{ThemeResource PivotHeaderItemCharacterSpacing}" 
        VerticalAlignment="Top"/>
    </StackPanel>

    <Maps:MapControl Grid.Row="1"/>
    <Grid Height="150"  Grid.Row="1" VerticalAlignment="Bottom" 
    Background="#FF7A2222" >
      <Interactivity:Interaction.Behaviors>
        <Behaviors:KeepFromBottomBehavior 
        WindowHeight="{Binding WatchedObjectHeight, 
        ElementName=ContentRootListener}"/>
      </Interactivity:Interaction.Behaviors>
      <TextBlock Text="Some popup"  
        Style="{ThemeResource TitleTextBlockStyle}" 
        VerticalAlignment="Bottom" HorizontalAlignment="Center" />
    </Grid>
  </Grid>
</Page

In bold and red you see the new parts. And sure enough, if you now watch the ‘popup’ it will always be above the App Bar – it will even move dynamically move up and down if you open and close the software buttons.

imageimage

Problem solved, case closed. You don’t even have to type code because the behavior is already in WpWinNl.

Mind you – this behavior works only on Windows Phone 8.1, since it only is applicable to Windows Phone. That’s because the ApplicationView.GetForCurrentView().VisibleBoundsChanged event is only available on Windows Phone.

Demo solutions – before and after adding NuGet Package and behaviors – can be found here. Mind you – both solutions contain a Windows 8.1 project, but that does not do anything.

Special thanks to my colleague Valentijn Makkenze for first noticing this problem, Ben Greeve and this guy for sending me screenshots, and Hermit Dave for some valuable pointers.