本来なら MVPVM で Presenter から View のイベントハンドラを設定したいところですが、あえて MVVM で、ViewModel から検知したいと思います
本家 XAML であればきっと EventToCommand みたいな Behavior を利用して検知するんでしょうが、今回は別の方法です
/// <summary> /// 画面状態監視サービス /// </summary> public class PageStateDetectService : IPageStateDetectService { /// <summary> /// 現在の画面 /// </summary> private Page currentPage = null; /// <summary> /// 現在の画面 /// </summary> public Page CurrentPage { get { return this.currentPage; } set { if (this.currentPage != null) { this.currentPage.Appearing -= this.OnPageAppearing; this.currentPage.SizeChanged -= this.OnPageSizeChanged; this.currentPage.Disappearing -= this.OnPageDisappearing; } this.currentPage = value; if (this.currentPage != null) { this.currentPage.Appearing += this.OnPageAppearing; this.currentPage.SizeChanged += this.OnPageSizeChanged; this.currentPage.Disappearing += this.OnPageDisappearing; } } } #region Events /// <summary> /// PageAppearing イベント /// </summary> public event PageStateChangedEventHandler PageAppearing; /// <summary> /// PageSizeChanged イベント /// </summary> public event PageStateChangedEventHandler PageSizeChanged; /// <summary> /// PageDisappearing イベント /// </summary> public event PageStateChangedEventHandler PageDisappearing; /// <summary> /// PageStateChanged イベントハンドラ delegate /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> public delegate void PageStateChangedEventHandler(object sender, PageStateChangedEventArgs e); /// <summary> /// 画面の Appearing イベントハンドラ /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void OnPageAppearing(object sender, EventArgs e) { if (this.PageAppearing == null) { return; } this.PageAppearing(this, new PageStateChangedEventArgs(new Size(this.currentPage.Width, this.currentPage.Height))); } /// <summary> /// 画面の SizeChanged イベントハンドラ /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void OnPageSizeChanged(object sender, EventArgs e) { if (this.PageSizeChanged == null) { return; } this.PageSizeChanged(this, new PageStateChangedEventArgs(new Size(this.currentPage.Width, this.currentPage.Height))); } /// <summary> /// 画面の Disappearing イベントハンドラ /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void OnPageDisappearing(object sender, EventArgs e) { if (this.PageDisappearing == null) { return; } this.PageDisappearing(this, new PageStateChangedEventArgs(new Size(this.currentPage.Width, this.currentPage.Height))); } #endregion //Events } /// <summary> /// 画面状態変更イベント引数 /// </summary> public class PageStateChangedEventArgs : EventArgs { /// <summary> /// 画面サイズ /// </summary> public Size PageSize { get; private set; } /// <summary> /// コンストラクタ /// </summary> /// <param name="size">画面サイズ</param> public PageStateChangedEventArgs(Size size) { this.PageSize = size; } }
まずは Page の Appearing や SizeChanged などのイベントを監視してイベントを発生させるサービスクラスを作ります
現在の画面として CurrentPage というプロパティを持たせてます
/// <summary> /// 画面状態監視用添付プロパティ付与クラス /// </summary> public static class PageStateDetect { /// <summary> /// Page の状態を監視するか否かを表す BindableProperty /// </summary> public static readonly BindableProperty DetectStateProperty = BindableProperty.Create( "DetectState", typeof(bool), typeof(PageStateDetect), false, BindingMode.TwoWay, null, DetectStateChanged); /// <summary> /// DetectStateProperty の変更イベントハンドラ /// </summary> /// <param name="bindable">BindableObject</param> /// <param name="oldValue">変更前の値</param> /// <param name="newValue">変更後の値</param> private static void DetectStateChanged(BindableObject bindable, object oldValue, object newValue) { var view = bindable as Page; if (view == null) { throw new Exception("Your views isn't Page"); } // Unity から画面状態監視サービスを解決して現在の Page を設定します App.Container.Resolve<IPageStateDetectService>().CurrentPage = view; } }
お次は XAML に指定する添付プロパティを持つ static クラスを作りました
下記のように Page の XAML に記述します
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:prismmvvm="clr-namespace:Prism.Mvvm;assembly=Prism.Mvvm.Xamarin" xmlns:cv="clr-namespace:XamarinUnityInjection.Converters;assembly=XamarinUnityInjection" xmlns:vm="clr-namespace:XamarinUnityInjection.ViewModels;assembly=XamarinUnityInjection" prismmvvm:ViewModelLocator.AutoWireViewModel="true" xmlns:local="clr-namespace:XamarinUnityInjection.Views;assembly=XamarinUnityInjection" local:PageStateDetect.DetectState="true" x:Class="XamarinUnityInjection.Views.TopPage"> <ContentPage.Resources> <ResourceDictionary> <cv:RectangleConverter x:Key="RectangleConverter"/> </ResourceDictionary> </ContentPage.Resources> <local:AbsoluteItemsControl HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" x:TypeArguments="vm:TickItemViewModel" ItemsSource="{Binding TickItems}"> <local:AbsoluteItemsControl.ItemTemplate> <DataTemplate> <BoxView Color="#7F4234" AbsoluteLayout.LayoutBounds="{Binding LayoutBounds, Converter={StaticResource RectangleConverter}}" AbsoluteLayout.LayoutFlags="None" AnchorX="0.5" AnchorY="0.5" Rotation="{Binding Rotation}"/> </DataTemplate> </local:AbsoluteItemsControl.ItemTemplate> </local:AbsoluteItemsControl> </ContentPage>
local:PageStateDetect.DetectState="true" と記述するだけで、PageStateDetect クラスに BindableObject が渡されます
そこから現在の Page を取得して PageStateDetectService.CurrentPage に設定すればいけそうです
これは Prism の ViewModelLocator と ViewModelLocationProvider の実装パターンと同じやり方です
あとは ViewModel から利用するだけ
/// <summary> /// 最初の画面の ViewModel /// </summary> public class TopPageViewModel : BindableBase { ~ 中略 ~ /// <summary> /// コンストラクタ /// </summary> /// <param name="pageStateDetectService">画面状態監視サービス(DI コンテナにより自動注入される)</param> public TopPageViewModel(IPageStateDetectService pageStateDetectService) { this.pageStateDetectService = pageStateDetectService; this.tickItems.Clear(); for (var i = 0; i < 60; i++) { this.tickItems.Add(App.Container.Resolve<TickItemViewModel>()); } this.pageStateDetectService.PageAppearing += this.OnPageAppearing; } /// <summary> /// 画面表示イベントハンドラ /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void OnPageAppearing(object sender, PageStateChangedEventArgs e) { this.pageStateDetectService.PageAppearing -= this.OnPageAppearing; this.Width = e.PageSize.Width; this.Height = e.PageSize.Height; this.Draw(); this.pageStateDetectService.PageSizeChanged += this.OnPageSizeChanged; } /// <summary> /// 画面サイズ変更イベントハンドラ /// </summary> /// <param name="sender">イベント発行者</param> /// <param name="e">イベント引数</param> private void OnPageSizeChanged(object sender, PageStateChangedEventArgs e) { this.Width = e.PageSize.Width; this.Height = e.PageSize.Height; this.Draw(); } /// <summary> /// 時計盤を描画します /// </summary> public void Draw() { if (this.width < 1 || this.height < 1) { return; } this.CenterX = this.width / 2; this.CenterY = this.height / 2; var radius = Math.Min(this.width, this.height); var index = 0; foreach (var tickItem in this.tickItems) { double size = 0.45 * radius / (index % 5 == 0 ? 15 : 30); double radians = index * 2 * Math.PI / this.tickItems.Count; tickItem.Left = this.centerX + 0.45 * radius * Math.Sin(radians) - size / 2; tickItem.Top = this.centerY - 0.45 * radius * Math.Cos(radians) - size / 2; tickItem.Width = size; tickItem.Height = size; tickItem.Rotation = 180 * radians / Math.PI; index++; } } }
Appearing や SizeChanged のタイミングで Width や Height を更新されるようにしています
サービスクラス経由で画面状態の変化を検知するので、View と疎結合です