【WP8】扩展CM的WindowManager
14-09-09更新:修复AppBar冲突bug
关于WindowManager,一直都很想写一篇博客分享一下,一直在忙别的,今天抽空把这个分享一下
在弹窗在移动开发是一个很常见的交互,很多时候我们都需要进行弹窗,比如我们需要询问用户的一些操作,提供更丰富的交互,又比如我们需要弹出一个框提示用户给我们好评
WP系统提供关于对话框的操作只有一个MessageBox,MessageBox样式简单,并且无法扩展,比如我们需要自定义按钮的文字都不可以,极大限制了我们的发挥,如果我们需要弹窗,还有Popup控件可以用,比如Coding4fun就对Popup进行了扩展,提供了更丰富对话框样式
用CM(Caluburn.Micro)也有一段时间了,CM不仅提供了MVVM的支持,还提供了IoC依赖注入容器,还提供了WindowManager的支持(这是一个非常值得深入研究的开源框架)可以在页面的ViewModel中直接通过控件对应的ViewModel构造控件进行显示,下面我们对控件进行
一、页面框架层次
首先说明一下页面框架的层次关系:
Frame:最底层
Page:页面在Frame的上层
Popup:在页面的上层,弹出的控件会在覆盖在页面的上面
Keyboard:键盘可以遮住Popup弹窗
ApplicationBar:应用程序栏在键盘之上,不会被Popup覆盖,所以自定义的弹窗是不能把ApplicationBar给遮住的
MessageBox:系统自带的MessageBox可以把一切遮住,包括ApplicationBar
二、WindowManager职责
WindowManager通过封装Popup,给为外部提供很方便的弹窗操作,下面列举一下WindowManager关于弹窗所做的操作(基于CM)
1、新建一个Host:当我们显示弹窗之前,我们需要一个容器(ContentControl)用于内部管理
2、通过ViewModel找到对应的View(控件),并且将ViewModel绑定到View上(CM)
3、Host.Content = view
4、ApplySetting:通过反射设置信息,CM默认的WindowManager提供了对Host的设置
5、判断ViewModel是否实现IActivate和IDeactivate事件,如果有,则绑定事件(在弹窗打开和关闭的时候触发)
6、接管BackKey:比如用户按返回键是否需要关闭弹窗
7、接管ApplicationBar:由于Popup在ApplicationBar下层,无法遮住ApplicationBar,一般的做法是隐藏掉ApplicationBar,或者是禁用掉ApplicationBar上的按钮和MenuItem,在弹窗关闭后,恢复ApplicationBar原有的状态
8、接管Page.OrientationChanged事件:一般我们不处理该事件
9、Popup.IsOpen = true; 打开弹窗(打开之前一般设置其Opacity为0,防止闪一下)
10、开启弹窗动画(CM默认没有提供动画的支持,后面我们自定义的时候加入该支持)
定义WindowManager主要就是上面一些操作,这里不分析CM中的WindowManager的源码了,有兴趣的可以去看,但是CM提供的WindowManager还是不够用,我们需要可以更多自定义的一些配置,比如:
1、弹窗的时候是否隐藏ApplicationBar
2、弹窗的时候点击其他区域是否关闭弹窗
3、弹窗是否需要用一个遮罩遮住原来页面
4、弹窗是否支持动画注入(支持扩展)
5、弹窗是否可以被返回键关闭
三、定义与实现
原本是想直接继承WindowManager来做的,但是发现WindowManager提供的属性和方法太少了,很难扩展,所以下面通过自定义的方式扩展
定义接口
public interface ICustomWindowManager : IWindowManager { void ShowDialog(object rootModel, bool isTapClose = true, double maskOpacity = 0.8, IWindowAnimator windowAnimator = null, bool isHideApplicationBar = true, bool canClose = true); }
实现:
动画接口
/// <summary> /// WindowManager动画的接口(用于扩展动画) /// </summary> public interface IWindowAnimator { void Enter(FrameworkElement viewContainer, FrameworkElement host); void Exit(FrameworkElement viewContainer, FrameworkElement host, Action complete); }
WindowAnimator动画默认实现
/// <summary> /// 默认弹窗动画的实现(渐入和渐出) /// </summary> public class DefaultWindowAnimator : IWindowAnimator { public void Enter(FrameworkElement viewContainer, FrameworkElement host) { var storyboard = new Storyboard(); var doubleAnimation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(0.1)), From = 0, To = 1 }; Storyboard.SetTarget(doubleAnimation, host); Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("Opacity", new object[0])); storyboard.Children.Add(doubleAnimation); storyboard.Begin(); } public void Exit(FrameworkElement viewContainer, FrameworkElement host, Action complete) { var storyboard = new Storyboard(); var doubleAnimation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(0.1)), From = 1, To = 0 }; Storyboard.SetTarget(doubleAnimation, host); Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("Opacity", new object[0])); storyboard.Children.Add(doubleAnimation); storyboard.Completed += (sender, e) => complete.Invoke(); storyboard.Begin(); } }
下面提供其他动画的实现
/// <summary> /// 翻转动画 /// </summary> public class FlipWindowAnimator : IWindowAnimator { private const double DURATION_SECONDS = 0.15; public void Enter(FrameworkElement viewContainer, FrameworkElement host) { viewContainer.Projection = new PlaneProjection(); var storyboard = new Storyboard(); var doubleAnimation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(DURATION_SECONDS)), From = 90, To = 0 }; Storyboard.SetTarget(doubleAnimation, viewContainer.Projection); Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("RotationX", new object[0])); storyboard.Children.Add(doubleAnimation); doubleAnimation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(DURATION_SECONDS)), From = 0, To = 1 }; Storyboard.SetTarget(doubleAnimation, host); Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("Opacity", new object[0])); storyboard.Children.Add(doubleAnimation); storyboard.Begin(); } public void Exit(FrameworkElement viewContainer, FrameworkElement host, Action complete) { var storyboard = new Storyboard(); var doubleAnimation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(DURATION_SECONDS)), From = 0, To = 90 }; Storyboard.SetTarget(doubleAnimation, viewContainer.Projection); Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("RotationX", new object[0])); storyboard.Children.Add(doubleAnimation); doubleAnimation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(DURATION_SECONDS)), From = 1, To = 0 }; Storyboard.SetTarget(doubleAnimation, host); Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath("Opacity", new object[0])); storyboard.Children.Add(doubleAnimation); storyboard.Completed += (sender, e) => complete.Invoke(); storyboard.Begin(); } }
滑动动画参考自WPToolkit
/// <summary> /// 上下滑动动画 /// </summary> public class SlideUpWindowAnimator : IWindowAnimator { /// <summary> /// 向上显示动画 /// </summary> private const string SLIDE_UP_STORYBOARD = @" <Storyboard xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=""(UIElement.RenderTransform).(CompositeTransform.TranslateY)""> <EasingDoubleKeyFrame KeyTime=""0"" Value=""150""/> <EasingDoubleKeyFrame KeyTime=""0:0:0.35"" Value=""0""> <EasingDoubleKeyFrame.EasingFunction> <ExponentialEase EasingMode=""EaseOut"" Exponent=""6""/> </EasingDoubleKeyFrame.EasingFunction> </EasingDoubleKeyFrame> </DoubleAnimationUsingKeyFrames> <DoubleAnimation Storyboard.TargetProperty=""(UIElement.Opacity)"" From=""0"" To=""1"" Duration=""0:0:0.350""> <DoubleAnimation.EasingFunction> <ExponentialEase EasingMode=""EaseOut"" Exponent=""6""/> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard>"; /// <summary> /// 向下消失动画 /// </summary> private const string SLIDE_DOWN_STORYBOARD = @" <Storyboard xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=""(UIElement.RenderTransform).(CompositeTransform.TranslateY)""> <EasingDoubleKeyFrame KeyTime=""0"" Value=""0""/> <EasingDoubleKeyFrame KeyTime=""0:0:0.25"" Value=""150""> <EasingDoubleKeyFrame.EasingFunction> <ExponentialEase EasingMode=""EaseIn"" Exponent=""6""/> </EasingDoubleKeyFrame.EasingFunction> </EasingDoubleKeyFrame> </DoubleAnimationUsingKeyFrames> <DoubleAnimation Storyboard.TargetProperty=""(UIElement.Opacity)"" From=""1"" To=""0"" Duration=""0:0:0.25""> <DoubleAnimation.EasingFunction> <ExponentialEase EasingMode=""EaseIn"" Exponent=""6""/> </DoubleAnimation.EasingFunction> </DoubleAnimation> </Storyboard>"; public void Enter(FrameworkElement viewContainer, FrameworkElement host) { var storyboard = XamlReader.Load(SLIDE_UP_STORYBOARD) as Storyboard; if (storyboard != null) { foreach (var t in storyboard.Children) { Storyboard.SetTarget(t, host); } storyboard.Begin(); } } public void Exit(FrameworkElement viewContainer, FrameworkElement host, Action complete) { var storyboard = XamlReader.Load(SLIDE_DOWN_STORYBOARD) as Storyboard; if (storyboard != null) { foreach (var t in storyboard.Children) { Storyboard.SetTarget(t, host); } storyboard.Completed += (sender, e) => complete.Invoke(); storyboard.Begin(); } } }
WindowManager实现:大部分参考自原来的WindowManager实现
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Markup; using System.Windows.Media; using Caliburn.Micro; using Microsoft.Phone.Controls; using Microsoft.Phone.Shell; namespace TestDemo { /// <summary> /// 自定义窗口管理器(基于Caliburn.Micro) /// </summary> public class CustomWindowManager : ICustomWindowManager { public static Func<Uri, bool> IsSystemDialogNavigation = uri => uri != null && uri.ToString().StartsWith("/Microsoft.Phone.Controls.Toolkit"); public virtual void ShowDialog(object rootModel, object context = null, IDictionary<string, object> settings = null) { var navigationSvc = IoC.Get<INavigationService>(); var host = new DialogHost(navigationSvc); var view = ViewLocator.LocateForModel(rootModel, host, context); host.Content = view as FrameworkElement; host.SetValue(View.IsGeneratedProperty, true); ViewModelBinder.Bind(rootModel, host, null); host.SetActionTarget(rootModel); ApplySettings(host, settings); var activatable = rootModel as IActivate; if (activatable != null) { activatable.Activate(); } var deactivator = rootModel as IDeactivate; if (deactivator != null) { host.Closed += delegate { deactivator.Deactivate(true); }; } host.Open(); } public virtual void ShowPopup(object rootModel, object context = null, IDictionary<string, object> settings = null) { var popup = CreatePopup(rootModel, settings); var view = ViewLocator.LocateForModel(rootModel, popup, context); popup.Child = view; popup.SetValue(View.IsGeneratedProperty, true); ViewModelBinder.Bind(rootModel, popup, null); var activatable = rootModel as IActivate; if (activatable != null) { activatable.Activate(); } var deactivator = rootModel as IDeactivate; if (deactivator != null) { popup.Closed += delegate { deactivator.Deactivate(true); }; } popup.IsOpen = true; } public void ShowDialog(object rootModel, bool isTapClose = true, double maskOpacity = 0.5, IWindowAnimator windowAnimator = null, bool isHideApplicationBar = true, bool canClose = true) { var navigationSvc = IoC.Get<INavigationService>(); var host = new DialogHost(navigationSvc, isTapClose, maskOpacity, isHideApplicationBar, windowAnimator, canClose); var view = ViewLocator.LocateForModel(rootModel, host, null); host.Content = view as FrameworkElement; host.SetValue(View.IsGeneratedProperty, true); ViewModelBinder.Bind(rootModel, host, null); host.SetActionTarget(rootModel); var activatable = rootModel as IActivate; if (activatable != null) { activatable.Activate(); } var deactivator = rootModel as IDeactivate; if (deactivator != null) { host.Closed += delegate { deactivator.Deactivate(true); }; } host.Open(); } protected virtual Popup CreatePopup(object rootModel, IDictionary<string, object> settings) { var popup = new Popup(); ApplySettings(popup, settings); return popup; } private static void ApplySettings(object target, IEnumerable<KeyValuePair<string, object>> settings) { if (settings != null) { var type = target.GetType(); foreach (var pair in settings) { var propertyInfo = type.GetProperty(pair.Key); if (propertyInfo != null) propertyInfo.SetValue(target, pair.Value, null); } } } [ContentProperty("Content")] public class DialogHost : FrameworkElement { readonly INavigationService navigationSvc; PhoneApplicationPage currentPage; Popup hostPopup; bool isOpen; ContentControl viewContainer; Border pageFreezingLayer; Border maskingLayer; private FrameworkElement host; private readonly IWindowAnimator animator; private readonly double maskOpacity; private readonly bool isTapClose; private readonly bool canClose; private readonly bool isHideApplicationBar; private readonly Dictionary<IApplicationBarIconButton, bool> appBarButtonsStatus = new Dictionary<IApplicationBarIconButton, bool>(); bool appBarMenuEnabled; public DialogHost(INavigationService navigationSvc, bool isTapClose = true, double maskOpacity = 0.5, bool isHideApplicationBar = true, IWindowAnimator animator = null, bool canClose = true) { this.navigationSvc = navigationSvc; this.canClose = canClose; currentPage = navigationSvc.CurrentContent as PhoneApplicationPage; if (currentPage == null) { throw new InvalidOperationException( string.Format("In order to use ShowDialog the view currently loaded in the application frame ({0})" + " should inherit from PhoneApplicationPage or one of its descendents.", navigationSvc.CurrentContent.GetType())); } navigationSvc.Navigating += OnNavigating; navigationSvc.Navigated += OnNavigated; this.maskOpacity = maskOpacity; this.isTapClose = isTapClose; this.isHideApplicationBar = isHideApplicationBar; CreateUiElements(); this.animator = animator ?? new DefaultWindowAnimator(); } public EventHandler Closed = delegate { }; public void SetActionTarget(object target) { Caliburn.Micro.Action.SetTarget(viewContainer, target); } public virtual FrameworkElement Content { get { return (FrameworkElement)viewContainer.Content; } set { viewContainer.Content = value; } } public void Open() { if (isOpen) { return; } isOpen = true; if (currentPage.ApplicationBar != null) { DisableAppBar(); } ArrangePlacement(); currentPage.BackKeyPress += CurrentPageBackKeyPress; currentPage.OrientationChanged += CurrentPageOrientationChanged; hostPopup.IsOpen = true; } public void Close() { Close(reopenOnBackNavigation: false); } void Close(bool reopenOnBackNavigation) { if (!isOpen) { return; } isOpen = false; animator.Exit(Content, host, () => { hostPopup.IsOpen = false; }); if (currentPage.ApplicationBar != null) { RestoreAppBar(); } currentPage.BackKeyPress -= CurrentPageBackKeyPress; currentPage.OrientationChanged -= CurrentPageOrientationChanged; if (!reopenOnBackNavigation) { navigationSvc.Navigating -= OnNavigating; navigationSvc.Navigated -= OnNavigated; Closed(this, EventArgs.Empty); } } protected IWindowAnimator CreateElementsAnimator() { return new DefaultWindowAnimator(); } protected void CreateUiElements() { var alpha = Convert.ToByte(maskOpacity * 255); viewContainer = new ContentControl { HorizontalContentAlignment = HorizontalAlignment.Stretch, VerticalContentAlignment = VerticalAlignment.Stretch, }; maskingLayer = new Border { Child = viewContainer, Background = new SolidColorBrush(Color.FromArgb(alpha, 0, 0, 0)), VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch, Width = Application.Current.Host.Content.ActualWidth, Height = Application.Current.Host.Content.ActualHeight }; if (isTapClose) { maskingLayer.Tap += (s, e) => { if (e.OriginalSource == maskingLayer) { Close(); } }; } pageFreezingLayer = new Border { Background = new SolidColorBrush(Colors.Transparent), Width = Application.Current.Host.Content.ActualWidth, Height = Application.Current.Host.Content.ActualHeight }; var panel = new Grid { RenderTransform = new CompositeTransform() }; panel.Children.Add(pageFreezingLayer); panel.Children.Add(maskingLayer); host = panel; hostPopup = new Popup { Child = panel }; } private bool applicationBarVisible; private void DisableAppBar() { if (isHideApplicationBar) { if (currentPage.ApplicationBar.IsVisible) { applicationBarVisible = currentPage.ApplicationBar.IsVisible; currentPage.ApplicationBar.IsVisible = false; } } else { appBarMenuEnabled = currentPage.ApplicationBar.IsMenuEnabled; appBarButtonsStatus.Clear(); currentPage.ApplicationBar.Buttons.Cast<IApplicationBarIconButton>() .Apply(b => { appBarButtonsStatus.Add(b, b.IsEnabled); b.IsEnabled = false; }); currentPage.ApplicationBar.IsMenuEnabled = false; } } private void RestoreAppBar() { if (isHideApplicationBar) { if (applicationBarVisible) { currentPage.ApplicationBar.IsVisible = applicationBarVisible; } } else { if (currentPage.ApplicationBar.IsMenuEnabled != appBarMenuEnabled) { currentPage.ApplicationBar.IsMenuEnabled = appBarMenuEnabled; currentPage.ApplicationBar.Buttons.Cast<IApplicationBarIconButton>() .Apply(b => { bool status; if (appBarButtonsStatus.TryGetValue(b, out status)) b.IsEnabled = status; }); } } } void ArrangePlacement() { //设置Opacity为0防止闪屏 host.Opacity = 0; maskingLayer.Dispatcher.BeginInvoke(() => animator.Enter(Content, host)); } Uri currentPageUri; void OnNavigating(object sender, System.Windows.Navigation.NavigatingCancelEventArgs e) { if (IsSystemDialogNavigation(e.Uri)) { currentPageUri = navigationSvc.CurrentSource; } } void OnNavigated(object sender, System.Windows.Navigation.NavigationEventArgs e) { if (IsSystemDialogNavigation(e.Uri)) { Close(currentPageUri != null); } else if (e.Uri.Equals(currentPageUri)) { currentPageUri = null; //refreshes the page instance currentPage = (PhoneApplicationPage)navigationSvc.CurrentContent; Open(); } else { Close(reopenOnBackNavigation: false); } } void CurrentPageBackKeyPress(object sender, CancelEventArgs e) { e.Cancel = true; if (canClose) { Close(); } } void CurrentPageOrientationChanged(object sender, OrientationChangedEventArgs e) { ArrangePlacement(); } } //TODO:待改 public void ShowDialog1(object rootModel, IWindowAnimator windowAnimator = null, bool isTapClose = true, double maskOpacity = 0.8, bool isHideApplicationBar = true, bool canClose = true) { } } }
四、使用
使用很简单,首先我们定义一个自定义窗口
<UserControl x:Class="XTuOne.Friday.Controls.Views.CommonDialogView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" VerticalAlignment="Top" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" d:DesignHeight="480" d:DesignWidth="480" mc:Ignorable="d"> <Grid Background="#1F1F1F"> <StackPanel Margin="12 48 12 12"> <TextBlock x:Name="Title" Margin="{StaticResource PhoneHorizontalMargin}" FontFamily="{StaticResource PhoneFontFamilySemiBold}" FontSize="{StaticResource PhoneFontSizeLarge}" Foreground="White" TextWrapping="Wrap" /> <TextBlock x:Name="Text" Margin="12 24" Foreground="White" Style="{StaticResource PhoneTextTitle3Style}" TextWrapping="Wrap" /> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Button x:Name="Ok" Grid.Column="0" BorderBrush="White" Foreground="White"> <TextBlock x:Name="OkText" Text="Ok" /> </Button> <Button x:Name="Cancel" Grid.Column="1" BorderBrush="White" Foreground="White"> <TextBlock x:Name="CancelText" Text="Cancel" /> </Button> </Grid> </StackPanel> </Grid> </UserControl>
后台没有内容,就不贴,然后定义控件对应的ViewModel
public enum DialogResult { Cancel, Ok, Close, } public class CommonDialogViewModel : Screen { //返回值 public DialogResult Result { get; private set; } //对话框的标题 public string Title { get; set; } //对话框的文字 public string Text { get; set; } //左边按钮的文字 public string OkText { get; set; } //右边按钮的文字 public string CancelText { get; set; } public CommonDialogViewModel() { Result = DialogResult.Close; OkText = "Ok"; CancelText = "Cancel"; } public void Ok() { Result = DialogResult.Ok; TryClose(); } public void Cancel() { Result = DialogResult.Cancel; TryClose(); } }
接下来是使用
var windowAnimator = new FlipWindowAnimator(); var customWindowManager = new CustomWindowManager(); var commonDialog = new CommonDialogViewModel { Title = "提示", Text = "该手机号已经注册过,您可以:", CancelText = "换个手机", OkText = "直接登录" }; commonDialog.Deactivated += (s, e) => { if (commonDialog.Result == DialogResult.Ok) { //用户点击左边按钮 } else if (commonDialog.Result == DialogResult.Cancel) { //用户点击右边按钮 } else { //非用户点击按钮关闭(用户点击返回键或离开App) } }; customWindowManager.ShowDialog(commonDialog, false, 0.8, flipWindowAnimator, false);
效果图
截图后面一张正在登陆的LoadingMask由于比较轻,没有用上面的WindowManager,我自己重新定义了一个PageMaskManager,下次再分享
注意:
1、为了防止多个弹窗导致HideApplicationBar冲突问题
2、由于这里是通过ApplicationBar.IsMenuEnable来判断是否被禁用的
3、所以请保证ApplicationBar.IsMenuEnable为true
WindowManager会改变ApplicationBar的绑定,如果使用了绑定,请注意 IsVisible,IsEnabled,IsMenuEnabled属性的绑定,因为这两个属性在WindowManager中被重新设置过
还有应该注意多个WindowManager同时使用时候的ApplicationBar冲突
个人能力有限,如果上文有误或者您有更好的实现,可以给我留言