The World’s Best MessageBox Control (it maybe just a bog standard message box control to you but I like it)

Thursday, December 09, 2010 / Posted by Luke Puplett /

Here it is in action, recorded on a Flip Mino HD, which really doesn’t like filming anything close-by or with fine detail. But you get the picture.

SimpleMessageBox for Windows Phone 7 from Luke Puplett on Vimeo.

The message box is a pretty straight forward affair, following the parts and states model. What’s possibly interesting is that the buttons are added as KeyValue<string, ICommand> pairs and rendered in the default template using a WrapPanel.

This means its dependant on two external libraries; Laurent Bugnion’s excellent MVVM Light Toolkit and also the Silverlight Toolkit for Windows Phone.

But the cool thing is that its entirely MVVM-able and the WrapPanel means that you can chuck as many buttons as you like on the page and they’ll look fabulous (darling).



namespace Evoq.Vuplan.Mobile.Phone.Controls
{
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Collections.Generic;
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;
using System;

/// <summary>
/// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
///
/// Step 1a) Using this custom control in a XAML file that exists in the current project.
/// Add this XmlNamespace attribute to the root element of the markup file where it is
/// to be used:
///
/// xmlns:MyNamespace="clr-namespace:AnimationExperiments"
///
///
/// Step 1b) Using this custom control in a XAML file that exists in a different project.
/// Add this XmlNamespace attribute to the root element of the markup file where it is
/// to be used:
///
/// xmlns:MyNamespace="clr-namespace:AnimationExperiments;assembly=AnimationExperiments"
///
/// You will also need to add a project reference from the project where the XAML file lives
/// to this project and Rebuild to avoid compilation errors:
///
/// Right click on the target project in the Solution Explorer and
/// "Add Reference"->"Projects"->[Browse to and select this project]
///
///
/// Step 2)
/// Go ahead and use your control in the XAML file.
///
/// <MyNamespace:WindowGrip/>
///
/// </summary>
[TemplatePart(Type = typeof(TextBlock), Name = SimpleMessageBox.TitlePartName)]
[TemplatePart(Type
= typeof(TextBlock), Name = SimpleMessageBox.MessagePartName)]
[TemplatePart(Type
= typeof(ItemsControl), Name = SimpleMessageBox.ButtonItemsPartName)]
[TemplateVisualState(GroupName
= "VisibilityStates", Name = SimpleMessageBox.CollapsedStateName)] // Default
[TemplateVisualState(GroupName = "VisibilityStates", Name = SimpleMessageBox.VisibleStateName)]
public class SimpleMessageBox : Control
{
#region Fields

string _currentState = "stateSetInOnApplyTemplate";

#endregion

#region Constants

private const string TitlePartName = "TitlePart";
private const string MessagePartName = "MessagePart";
private const string ButtonItemsPartName = "ButtonItemsPart";

private const string CollapsedStateName = "Collapsed";
private const string VisibleStateName = "Visible";

#endregion

#region Dependency Property Backers

public static readonly DependencyProperty IsCollapsedProperty =
DependencyProperty.Register(
"IsCollapsed", typeof(bool), typeof(SimpleMessageBox),
new PropertyMetadata(true, new PropertyChangedCallback(HandleCollapsedChanged)));

public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(
"Title", typeof(string), typeof(SimpleMessageBox),
new PropertyMetadata("Alert"));

public static readonly DependencyProperty MessageProperty =
DependencyProperty.Register(
"Message", typeof(string), typeof(SimpleMessageBox),
new PropertyMetadata("You need to set a message to display, even if it is an empty string."));

public static readonly DependencyProperty ButtonsProperty =
DependencyProperty.Register(
"Buttons", typeof(ObservableCollection<KeyValuePair<string, ICommand>>), typeof(SimpleMessageBox),
new PropertyMetadata(new ObservableCollection<KeyValuePair<string, ICommand>>()));

#endregion

#region Constructors

static SimpleMessageBox()
{
// DefaultStyleKeyProperty.OverrideMetadata(typeof(StatefulImage), new FrameworkPropertyMetadata(typeof(StatefulImage)));
}

public SimpleMessageBox()
:
base()
{
DefaultStyleKey
= this.GetType();
this.Buttons = new ObservableCollection<KeyValuePair<string, ICommand>>();

if (DesignerHelper.GetIsInDesignMode())
{
this.Buttons.Add(new KeyValuePair<string, ICommand>("test", new RelayCommand(new Action(() => { }))));
}
}

#endregion

#region Properties

/// <summary>
/// Gets or sets a value that indicates whether or not the message box is collapsed.
/// </summary>
public bool IsCollapsed
{
get { return (bool)GetValue(IsCollapsedProperty); }
set { SetValue(IsCollapsedProperty, value); }
}

/// <summary>
/// Gets or sets the title of the message box.
/// </summary>
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}

/// <summary>
/// Gets or sets the message in the message box.
/// </summary>
public string Message
{
get { return (string)GetValue(MessageProperty); }
set { SetValue(MessageProperty, value); }
}

/// <summary>
/// Gets or sets a collection of the buttons on the message box.
/// </summary>
public ObservableCollection<KeyValuePair<string, ICommand>> Buttons
{
get { return (ObservableCollection<KeyValuePair<string, ICommand>>)GetValue(ButtonsProperty); }
set { SetValue(ButtonsProperty, value); }
}

#endregion

#region Methods

private static void HandleCollapsedChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var control
= (SimpleMessageBox)sender;

control.UpdateStates(
true);
}

/// <summary>
/// Reads properties and fires off the state changer using VSM.
/// </summary>
/// <param name="useTransitions">Whether to trigger animations between the states.</param>
private void UpdateStates(bool useTransitions)
{
string state = this.IsCollapsed ? CollapsedStateName : VisibleStateName;

if (_currentState != state)
{
_currentState
= state;
VisualStateManager.GoToState(
this, state, useTransitions);
}
}

void page_BackKeyPress(object sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel
= !this.IsCollapsed;
this.IsCollapsed = true;
}

#endregion

#region Control Overrides

public override void OnApplyTemplate()
{
base.OnApplyTemplate();

this.UpdateStates(false);

try
{
FrameworkElement p
= this;
while (!(p is PhoneApplicationPage))
p
= p.Parent as FrameworkElement;

var page
= (PhoneApplicationPage)p;
page.BackKeyPress
+= new EventHandler<System.ComponentModel.CancelEventArgs>(page_BackKeyPress);
}
catch
{ }
}

#endregion
}
}

This should be all that’s needed in the Themes\generic.xaml to set its default skin.





<ResourceDictionary
xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:windows
="clr-namespace:System.Windows;assembly=System.Windows"
xmlns:local
="clr-namespace:YourControlNamespace;assembly=YourControlsAssembly"
xmlns:toolkit
="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"
xmlns:i
="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd
="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WP7"
xmlns:unsupported
="clr-namespace:Microsoft.Phone.Controls.Unsupported"
>
<!-- Resource dictionary entries should be defined here. -->

<Style TargetType="local:SimpleMessageBox">
<Setter Property="Background" Value="{StaticResource PhoneChromeBrush}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SimpleMessageBox">
<Canvas>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisibilityStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.3">
<VisualTransition.GeneratedEasingFunction>
<BackEase EasingMode="EaseOut"/>
</VisualTransition.GeneratedEasingFunction>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Collapsed">
<Storyboard>
<DoubleAnimation Duration="0" To="-90" Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationX)" Storyboard.TargetName="MessageBoard" />
</Storyboard>
</VisualState>
<VisualState x:Name="Visible"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="MessageBoard" Background="{TemplateBinding Background}" Width="{TemplateBinding Width}">
<Grid.Projection>
<PlaneProjection/>
</Grid.Projection>
<StackPanel Margin="15">
<TextBlock x:Name="TitlePart" FontSize="{StaticResource PhoneFontSizeLarge}" FontFamily="{StaticResource PhoneFontFamilyNormal}"
Text
="{TemplateBinding Title}" TextWrapping="Wrap" Margin="0,0,0,5" />
<TextBlock x:Name="MessagePart" FontSize="{StaticResource PhoneFontSizeMedium}" FontFamily="{StaticResource PhoneFontFamilyLight}"
Text
="{TemplateBinding Message}" TextWrapping="Wrap" Margin="0,0,0,16" />
<ItemsControl x:Name="ButtonItemsPart" ItemsSource="{TemplateBinding Buttons}" Margin="-15,0" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<toolkit:WrapPanel Orientation="Horizontal" HorizontalAlignment="Left" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button MinWidth="238">
<TextBlock Text="{Binding Key}" />
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<cmd:EventToCommand Command="{Binding Value}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>


</ResourceDictionary>




And there are two snippets here that demonstrate the control, but are otherwise not required.





// In Page ViewModel ctor or some method.

// Tests the control by constantly transitioning.

this.MessageBox = new MessageBoxViewModel()
{
Title
= "Test run-time data",
Message
= "This message text is inserted in the constructor of the view model.",
ControlVisibility
= Visibility.Visible,
};

var exit
= new RelayCommand( () => { _t.Change(0, 0); } );

this.MessageBox.ButtonCommandPairs.Add(new KeyValuePair<string, System.Windows.Input.ICommand>("run", null));
this.MessageBox.ButtonCommandPairs.Add(new KeyValuePair<string, System.Windows.Input.ICommand>("time", exit));

_t
= new System.Threading.Timer(new System.Threading.TimerCallback((state) =>
{
_log.Debug(
"Timer");

if (this.MessageBox.IsCollapsed)
Deployment.Current.Dispatcher.BeginInvoke( ()
=> this.MessageBox.IsCollapsed = false );
else
Deployment.Current.Dispatcher.BeginInvoke(()
=> this.MessageBox.IsCollapsed = true);

}),
null, 1000, 1000);


// In ViewModel somewhere:

public MessageBoxViewModel MessageBox { get; set; }




This is the view model for the message box and is usually exposed as a property of the page’s view model (note above the this.MessageBox property being set).





namespace Evoq.Vuplan.Mobile.Phone.ViewModels
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

public class MessageBoxViewModel : INotifyPropertyChanged
{
#region Fields

#endregion

#region Events and OnMethods

public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler
= PropertyChanged;
if (null != handler)
{
handler(
this, new PropertyChangedEventArgs(propertyName));
}
}

#endregion

#region Constructors

public MessageBoxViewModel()
{
this.ButtonCommandPairs = new ObservableCollection<KeyValuePair<string, ICommand>>();
}

#endregion

#region Properties

private string _title;
/// <summary>
/// Sample ViewModel property; this property is used in the view to display its value using a Binding.
/// </summary>
/// <returns></returns>
public string Title
{
get
{
return _title;
}
set
{
if (value != _title)
{
_title
= value;
NotifyPropertyChanged(
"Title");
}
}
}

private string _message;
/// <summary>
/// Sample ViewModel property; this property is used in the view to display its value using a Binding.
/// </summary>
/// <returns></returns>
public string Message
{
get
{
return _message;
}
set
{
if (value != _message)
{
_message
= value;
NotifyPropertyChanged(
"Message");
}
}
}

private bool _isCollapsed;
/// <summary>
/// Sample ViewModel property; this property is used in the view to display its value using a Binding.
/// </summary>
public bool IsCollapsed
{
get
{
return _isCollapsed;
}
set
{
if (value != _isCollapsed)
{
_isCollapsed
= value;
NotifyPropertyChanged(
"IsCollapsed");
}
}
}

private Visibility _controlVisibility;
/// <summary>
/// Sample ViewModel property; this property is used in the view to display its value using a Binding.
/// </summary>
/// <returns></returns>
public Visibility ControlVisibility
{
get
{
return _controlVisibility;
}
set
{
if (value != _controlVisibility)
{
_controlVisibility
= value;
NotifyPropertyChanged(
"ControlVisibility");
}
}
}

public ObservableCollection<KeyValuePair<string, ICommand>> ButtonCommandPairs { get; private set; }

#endregion
}
}


This lump of XAML shows how to declare the control on a page. The thing which is important here is to use TwoWay binding because the SimpleMessageBox control itself can change the IsCollapsed property internally when handling the back button key.





<controllib:SimpleMessageBox DataContext="{Binding MessageBox}"
Title
="{Binding Title}" Message="{Binding Message}"
Buttons
="{Binding ButtonCommandPairs}"
Width
="{Binding ElementName=LayoutRoot, Path=ActualWidth}"
IsCollapsed
="{Binding IsCollapsed, Mode=TwoWay}" />




Merry Christmas!

2 comments:

Comment by Luke Puplett on Thursday, December 09, 2010

Just to clarify, to add buttons you just add KeyValuePair where the string is the text on the button and the ICommand is a RelayCommand from the MVVM Toolkit. It will align the buttons perfectly on a portrait page, not tried rotating it, steering clear of that headache for now ;)

Comment by Luke Puplett on Thursday, December 09, 2010

Sorry, I thought my gt lt symbols would be automatically escaped.

KeyValuePair[string, ICommand] I can't remember how to escape chevrons. Have I ever told you how much I hate HTML and CSS?

Post a Comment