Visually Located

XAML and GIS

Update: Creating a custom MessageBox for your Windows Phone apps.

I was looking at my site traffic and noticed that one of my most popular posts was about creating a custom MessageBox. This post was written two years ago and continues to get a lot of traffic. Since writing that post, I’ve updated my MessageBox a lot. I’ve changed it to use async/await, modified the style, corrected some bugs, and added functionality. Since I’ve made a lot of changes, and that post continues to get a lot of readers, I thought it would be good to give the latest version.

I continue to use a UserControl approach for this because I don’t want any overriding of styles. It has a set look, and should not be allowed to be changed (aside from the use of static resources). The xaml only need a small change to the bottom margin. Instead of 12 all around, it needed 18 on the bottom. I also changed the name of the first Grid from MessagePanel to LayoutRoot, this wasn’t needed, but made some code behind easier to understand what element was being modified.

<Grid x:Class="Pinnacle.Controls.MessageBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"             
             Margin="0">
    <Grid.Background>
        <SolidColorBrush Color="{StaticResource PhoneBackgroundColor}" Opacity=".5"/>
    </Grid.Background>
    <Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}"
          VerticalAlignment="Top" HorizontalAlignment="Stretch"
          toolkit:TiltEffect.IsTiltEnabled="True" >
        <StackPanel x:Name="MessagePanel" Margin="12">
            <TextBlock x:Name="HeaderTextBlock" TextWrapping="Wrap"
                       Style="{StaticResource PhoneTextLargeStyle}"
                       FontFamily="{StaticResource PhoneFontFamilySemiBold}"
                       HorizontalAlignment="Left"/>
            <TextBlock x:Name="MessageTextBlock" TextWrapping="Wrap"
                       Style="{StaticResource PhoneTextNormalStyle}"
                       FontSize="{StaticResource PhoneFontSizeMedium}"
                       Margin="12,24,12,24"
                       HorizontalAlignment="Left"/>
            <Grid HorizontalAlignment="Stretch" Margin="0,6,0,0">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Button x:Name="YesButton" Click="YesButton_Click"/>
                <Button x:Name="NoButton" Grid.Column="1" Click="NoButton_Click"/>
            </Grid>
        </StackPanel>
    </Grid>
</Grid>

The code behind saw a lot of changes as can be seen in this diff comparison from WinMerge.

image

The first change was to make the Show method awaitable. This requires removing the Closed event, and the use of a TaskCompletionSource. We need to wait awhile for user interaction, so we’ll create a member wide TaskCompletionSource. Bold is for new code.

private readonly TaskCompletionSource<MessageBoxResult> _completeionSource;
 
private MessageBox()
{
    InitializeComponent();
    _completeionSource = new TaskCompletionSource<MessageBoxResult>();
}
 
public static Task<MessageBoxResult> ShowAsync(string message, string caption, string yesButtonText, string noButtonText = null)
{
    MessageBox msgBox = new MessageBox();
    msgBox.HeaderTextBlock.Text = caption;
    msgBox.MessageTextBlock.Text = message;
    msgBox.YesButton.Content = yesButtonText;
    if (string.IsNullOrWhiteSpace(noButtonText))
    {
        msgBox.NoButton.Visibility = Visibility.Collapsed;
    }
    else
    {
        msgBox.NoButton.Content = noButtonText;
    }
    msgBox.Insert();
 
    return msgBox._completeionSource.Task;
}

The previous MessageBox fired the Closed event and then removed the MessageBox from the active page. When changing to using the TaskCompletionSource, I changed to set the result after the MessageBox was removed from the page. Within the Completed event handler of the transition, set the result.

_completeionSource.SetResult(result);

I intentionally left out most of the code of the remove method because I wanted to save the best for last. The last bit that I added is something no other custom MessageBox does. We need to account for apps that have changed the SystemTray of the page.

The MessageBox needs to change the BackgroundColor of the SystemTray to be the PhoneChromeBrush color. If the user changes the Opacity of the SystemTray to be less than 1 the MessageBox needs to account for it. If the Opacity is less than one, then the page will be shifted up into the tray. This has always bugged me about the Opacity property of the SystemTray and the ApplicationBar. Rather than just changing the opacity (whether you can see behind it), it also changes  the page layout by shifting the page content up. If you show any other custom MessageBox in this state, you will see the background of the page in the SystemTray rather than seeing the PhoneChromeBrush resource color. To account for this, We need to detect if the tray has an opacity. If it does, add a large margin to the top.

private void Insert()
{
    // Make an assumption that this is within a phone application that is developed "normally"
    var frame = Application.Current.RootVisual as Microsoft.Phone.Controls.PhoneApplicationFrame;
    _page = frame.Content as PhoneApplicationPage;
 
    // store the original color, and change the tray to the chrome brush
    _originalTrayColor = SystemTray.BackgroundColor;
    SystemTray.BackgroundColor = ((SolidColorBrush)Application.Current.Resources["PhoneChromeBrush"]).Color;
 
    bool shouldBuffer = SystemTray.Opacity < 1;
    if (shouldBuffer)
    {
        // adjust the margin to account for the page shifting up
        Margin = new Thickness(0, -32, 0, 0);
        var margin = MessagePanel.Margin;
        MessagePanel.Margin = new Thickness(margin.Left, 64, margin.Right, margin.Bottom);
    }
 
    _page.BackKeyPress += Page_BackKeyPress;
 
    // assume the child is a Grid, span all of the rows
    var grid = System.Windows.Media.VisualTreeHelper.GetChild(_page, 0) as Grid;
    if (grid.RowDefinitions.Count > 0)
    {
        Grid.SetRowSpan(this, grid.RowDefinitions.Count);
    }
    grid.Children.Add(this);
 
    // Create a transition like the regular MessageBox
    SwivelTransition transitionIn = new SwivelTransition();
    transitionIn.Mode = SwivelTransitionMode.BackwardIn;
 
    ITransition transition = transitionIn.GetTransition(LayoutRoot);
    EventHandler transitionCompletedHandler = null;
    transitionCompletedHandler = (s, e) =>
    {
        transition.Completed -= transitionCompletedHandler;
        transition.Stop();
    };
    transition.Completed += transitionCompletedHandler;
    transition.Begin();
 
    if (_page.ApplicationBar != null)
    {
        // Hide the app bar so they cannot open more message boxes
        _page.ApplicationBar.IsVisible = false;
    }
}

When the MessageBox is removed, we then need to set the BackgroundColor back to it’s original value.

private void Remove(MessageBoxResult result)
{
    _page.BackKeyPress -= Page_BackKeyPress;
 
    var frame = (PhoneApplicationFrame)Application.Current.RootVisual;
    var page = frame.Content as PhoneApplicationPage;
    var grid = VisualTreeHelper.GetChild(page, 0) as Grid;
 
    // Create a transition like the regular MessageBox
    SwivelTransition transitionOut = new SwivelTransition();
    transitionOut.Mode = SwivelTransitionMode.BackwardOut;
 
    ITransition transition = transitionOut.GetTransition(LayoutRoot);
    EventHandler transitionCompletedHandler = null;
    transitionCompletedHandler = (s, e) =>
    {
        transition.Completed -= transitionCompletedHandler;
        SystemTray.BackgroundColor = _originalTrayColor;
        transition.Stop();
        grid.Children.Remove(this);
        if (page.ApplicationBar != null)
        {
            page.ApplicationBar.IsVisible = true;
        }
        _completeionSource.SetResult(result);
    };
    transition.Completed += transitionCompletedHandler;
    transition.Begin();
}

In both methods, I also corrected a memory leak with the event handlers. Always remember to unsubscribe from those inline events! As always, here is a zip of the source.

Use the [beta] Nokia Imaging SDK to crop and resize any image to create a lockscreen for your phone

When the new Nokia Imaging SDK was released I was really excited to start using it within one of my apps. Unlike most, I was not interested in the image filters that change how it looks. I was initially interested in using the resize and crop functionality it had. The day after it was released my wife had surgery, so I had a good amount of time to play with the SDK while I sat in the waiting room. What I wanted to accomplish that was to take a random photo from the users phone, crop and resize it to fit the device and set it as the lockscreen. I know that you can set any image to be the lockscreen and  if the image is too big, it will center the image. I needed to do it manually because I wanted to overlay information on the image.

Getting the random image is pretty easy. We’ll just get one from the MediaLibrary.

private Picture GetRandomImage()
{
    var rand = new Random(DateTime.Now.Millisecond);
 
    MediaLibrary library = new MediaLibrary();
    var album = library.RootPictureAlbum;
 
    int albumIndex = rand.Next(0, album.Albums.Count - 1);
    album = album.Albums[albumIndex];
 
    var pictureIndex = rand.Next(0, album.Pictures.Count - 1);
    var picture = album.Pictures[pictureIndex];
 
    return picture;
}

Now that we have a Picture, we need to crop and resize it. All actions with an image are done through an EditingSession. You create an EditingSession with the EditingSessionFactory. The EditingSession has two key methods that allow us to crop and/or resize any image to fit the phone screen. The AddFilter method will allow us to crop the image while the RenderToJpegAsync method will allow us to resize the image. The AddFilter method is bar far the best part of the SDK. It allows you to do anything to the image., and I do mean anything. The method takes an IFilter as an argument. Lucky for us you can create a cropping filter. To create a cropping filter, you need to specify an area that will be the crop.

/// <summary>
/// Returns the area needed to crop an image to the desired height and width.
/// </summary>
/// <param name="imageSize">The size of the image.</param>
/// <param name="desiredSize">The desired size to crop the image to.</param>
/// <returns></returns>
private static Rect? GetCropArea(Size imageSize, Size desiredSize)
{
    // how big is the picture compared to the phone?
    var widthRatio = desiredSize.Width / imageSize.Width;
    var heightRatio = desiredSize.Height / imageSize.Height;
 
    // the ratio is the same, no need to crop it
    if (widthRatio == heightRatio) return null;
 
    double cropWidth;
    double cropheight;
    if (widthRatio < heightRatio)
    {
        cropheight = imageSize.Height;
        cropWidth = desiredSize.Width / heightRatio;
    }
    else
    {
        cropheight = desiredSize.Height / widthRatio;
        cropWidth = imageSize.Width;
    }
 
    int left = (int)(imageSize.Width - cropWidth) / 2;
    int top = (int)(imageSize.Height - cropheight) / 2;
 
    var rect = new Windows.Foundation.Rect(left, top, cropWidth, cropheight);
    return rect;
}
I like to keep things generic and reusable, so the method above will crop any image to any size. If the desired size is larger than the image size it will crop to the same dimensions. 

Now, given the Picture from the MediaLibrary, we can crop it using the Nokia Imaging SDK.

/// <summary>
/// Crops a Picture to the desired size.
/// </summary>
/// <param name="picture">The Picture to crop.</param>
/// <returns>A copy of the Picture which is cropped.</returns>
private static async Task<IBuffer> CropPicture(Picture picture, Size desiredSize)
{
    using (var stream = picture.GetImage())
    {
        using (EditingSession session = await EditingSessionFactory.CreateEditingSessionAsync(stream))
        {
            // Get the crop area of the image, we need to ensure that
            // the image does not get skewed
            Rect? rect = GetCropArea(new Size(picture.Width, picture.Height), desiredSize);
            if (rect.HasValue)
            {
                IFilter filter = FilterFactory.CreateCropFilter(rect.Value);
                session.AddFilter(filter);
            }
 
            // We always want the image to be the size of the phone. 
            // That may mean that it needs to be scaled up also
            var finalImageSize = new Size(desiredSize.Width, desiredSize.Height);
 
            return await session.RenderToJpegAsync(finalImageSize, OutputOption.PreserveAspectRatio);
        }
    }
}

Again, the CropPicture method is generic, allowing for any size. This method could easily be an extension method on the Picture. Now we need to wrap everything up by calling these methods with the phone size. There are already examples of getting the resolution of the phone, so I won’t go into that. Now just put the pieces together.

private async Task<Stream> GetRandomLockscreen()
{
    // Get the width and height of the phone
    double phoneWidth = Resolution.PhoneWidth;
    double phoneHeight = Resolution.PhoneHeight;
 
    Picture mediaPicture  = GetRandomImage();
    IBuffer croppedImage = await CropPicture(mediaPicture, new Size(phoneWidth, phoneHeight));
 
    if (croppedImage == null) return null;
 
    return croppedImage.AsStream();
}

From there you can save the image to disk, and then maybe save it as a lockscreen. While this code works great, do not expect it to work in a background task. Here is the one place where the imaging SDK falls short. I was wanting to use the SDK in a background task. Background tasks are limited on the amount of resources allowed. The biggest limitation is the memory constraint. Working with images is very memory intensive, especially in C#. So when trying to manipulate an image in a background task, you can quickly run out of memory. I was hoping the imaging SDK would help alleviate some of these issues by working with images smarter. Instead of reading in every pixel of the image and then processing it, it should read n rows of pixels from the stream and then process those rows. This is a hard thing to accomplish, but I’m hoping the awesome people at Nokia can update their SDK to allow for this.