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:
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 ;)
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