ViewModelから画面遷移する[Xamarin.Forms]
様の投稿を見て思いつきました。ネタというか個人的に好きな方法で実用性はわかりません。
MVVMでViewModelからViewへの操作は難しいです。理由は上記投稿を見ていただければ大丈夫なので説明はしません。
ただ、個人的にViewへのコードビハインドは何も書きたくない(願望)ですし、なんとなくViewModelからViewを生成したくない、画面遷移だけでライブラリをいれたくないという超わがままな理由から第5の方法を紹介します。
それは
Messengerを自作する
です。
いやいやMessagingCenterがあるじゃん!!思うかもしれませんがMessagingCenterはstaticであり思わぬところで誤爆する可能性があ・・・りますかね? あとどうせなら自分で作ってカスタマイズしたいですよねそうですよね!!
では実際に作っていきましょう。
Messenger
今回は簡単なMessengerを作ります。
作り方は
を参考にしています。というかほとんど同じです。この記事はWPFの解説記事ですし書かれた時期はXamarin社ができたのとほぼ同じですがC#の基本的な機能を使っているのでView部分以外はそのまま使えます。素晴らしい!!
Message.cs
MessengerのMessage部分です。ここにViewへのメッセージを詰め込みます。今回はResponseを受け取る予定はないのでデフォルト引数でnullを指定しています。
using System; namespace MessengerSample { public class Message { //メッセージ本体 public object Body { get;} //レスポンス(今回は未使用) public object Response { get; } public Message(object body, object response=null) { this.Body=body; Response = response; } } }
MessageEventArgs.cs
Messengerのイベントのイベント引数です。今回は特にコールバックを以下略
using System; namespace MessengerSample { public class MessageEventArgs:EventArgs { public Message Message { get; } public Action<Message> Callback { get;} public MessageEventArgs(Message message, Action<Message> callback=null) { this.Message = message; this.Callback = callback; } } }
Messenger.cs
Messageを発行するクラスです。今回は特にコールバックを(ry
using System; namespace MessengerSample { public class Messenger { //Messageが送信されたことを通知するイベント public event EventHandler<MessageEventArgs> Raised; //Messageを送信する public void Raise(Message message, Action<Message> callback=null) { this.Raised?.Invoke(this,new MessageEventArgs(message,callback)); } } }
ViewModelBase.cs
これでMessengerの用意ができたのでこれを使っていくクラスを作っていきます。
using System.ComponentModel; using System.Runtime.CompilerServices; using MessengerSample.Annotations; namespace MessengerSample { public class ViewModelBase:INotifyPropertyChanged { public Messenger Messenger { get; }=new Messenger(); public event PropertyChangedEventHandler PropertyChanged; [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }
このViewModelBaseを継承することでMessengerを使ってViewに通知をだすことができます。 あとなぜかINotifyPropertyChangedを継承していますが特に必要ありません。
MessageTriggerBehavior.cs
次にMessageを受信します。上でも述べたとおりViewのコードビハインドに書くのは避けたいのでBehaviorを使います。
using System; using Xamarin.Forms; namespace MessengerSample { public class MessageTriggerBehavior : Behavior<Page> { private Page _page; public static readonly BindableProperty MessengerProperty = BindableProperty.Create(nameof(Messenger), typeof (Messenger), typeof (MessageTriggerBehavior), default(Messenger)); //ViewModelのMessenger public Messenger Messenger { get { return (Messenger) GetValue(MessengerProperty); } set { SetValue(MessengerProperty, value); } } public static readonly BindableProperty MessageKeyProperty = BindableProperty.Create(nameof(MessageKey), typeof (string), typeof (MessageTriggerBehavior), default(string)); //このMessageKeyと一致したとき遷移させる public string MessageKey { get { return (string) GetValue(MessageKeyProperty); } set { SetValue(MessageKeyProperty, value); } } public static readonly BindableProperty PageTypeProperty = BindableProperty.Create(nameof(PageType), typeof (Type), typeof (MessageTriggerBehavior), default(Type)); //遷移先ページのタイプ public Type PageType { get { return (Type) GetValue(PageTypeProperty); } set { SetValue(PageTypeProperty, value); } } protected override void OnAttachedTo(Page bindable) { base.OnAttachedTo(bindable); _page = bindable; //これしないとBehaviorのプロパティに値がバインドされない this.BindingContext = bindable.BindingContext; //Messengerに登録 Messenger.Raised += Invoke; } protected override void OnDetachingFrom(Page bindable) { Messenger.Raised -= Invoke; this.BindingContext = null; base.OnDetachingFrom(bindable); } private void Invoke(object sender, MessageEventArgs args) { var message = args.Message.Body as string; //MessageKeyと送られてきたMessage.Bodyがあっていなければ何もしない if (message == null || MessageKey != message) return; //PageTypeからPageインスタンス作成 var page = Activator.CreateInstance(PageType) as Page; //遷移 _page.Navigation.PushAsync(page); } } }
MessengerにはViewModelBaseのMessengerをバインドし、MessengerKeyにはMessengerのイベントハンドラに登録したMessage.Bodyと同じ文字列を指定します。
MessageKeyと同じMessage.Bodyを受信するとPageTypeからインスタンスを生成し、Navigation.PushAsyncする単純な仕組みです。
これで準備が終わりました。長い
使ってみる
Content1.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:messengerSample="clr-namespace:MessengerSample;assembly=MessengerSample" x:Class="MessengerSample.Content1"> <ContentPage.BindingContext> <messengerSample:ContentViewModel /> </ContentPage.BindingContext> <ContentPage.Behaviors> <messengerSample:MessageTriggerBehavior Messenger="{Binding Messenger}" MessageKey="Show:Content2" PageType="{x:Type messengerSample:Content2}" /> </ContentPage.Behaviors> <StackLayout HorizontalOptions="Center" VerticalOptions="Center"> <Label Text="Content1" FontSize="Large" /> <Button Text="Navigate Content2" Command="{Binding NavigationContent2}" /> </StackLayout> </ContentPage>
Content2.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:messengerSample="clr-namespace:MessengerSample;assembly=MessengerSample" x:Class="MessengerSample.Content2"> <ContentPage.BindingContext> <messengerSample:ContentViewModel /> </ContentPage.BindingContext> <ContentPage.Behaviors> <messengerSample:MessageTriggerBehavior Messenger="{Binding Messenger}" MessageKey="Show:Content1" PageType="{x:Type messengerSample:Content1}" /> </ContentPage.Behaviors> <StackLayout HorizontalOptions="Center" VerticalOptions="Center"> <Label Text="Content2" FontSize="Large" TextColor="Green" /> <Button Text="Navigation Content1" Command="{Binding NavigationContent1}" /> </StackLayout> </ContentPage>
ContentViewModel.cs
using System; using Reactive.Bindings; namespace MessengerSample { public class ContentViewModel : ViewModelBase { //Content1に遷移するコマンド public ReactiveCommand NavigationContent1 { get; } = new ReactiveCommand(); //Content2に遷移するコマンド public ReactiveCommand NavigationContent2 { get; } = new ReactiveCommand(); public ContentViewModel() { NavigationContent1.Subscribe(_ => Messenger.Raise(new Message("Show:Content1"))); NavigationContent2.Subscribe(_ => Messenger.Raise(new Message("Show:Content2"))); } } }
Content1のボタンをクリックするとCommandが実行しViewに向けてメッセージが飛びます。それをBehaviorで受け止めてリフレクションでContent2のインスタンスを作成し遷移させます。
今回はわざわざICommandを継承したクラスを作成して〜をするのがめんどくさかったので ReactiveProperty でコマンドを生成しています。
これを実行すると・・・
Navigation Content2を押せば
無事遷移できました!! ちなみにサンプルがiOSだけなのはAndroidのシュミレータはなぜか起動しないし(実機持ってない)、UWPは’アプリは開始されませんでした’エラーでいつもどおり実行できなかったので実行できたのがiOSだけだからです。
画面遷移時のデータ引き渡しについて
画面遷移時のデータ引き渡しにはMessageを拡張し、そこに遷移先のViewModelのインスタンスを突っ込んでRaiseしMessageTriggerBehaviorでごにょごにょすれば大丈夫だと思います。
まとめ
ライブラリでいいんじゃね?
この方法でも実際に画面遷移ができました。この方法の利点は自分で好きな機能を継ぎ足せることにあると思います。
また余計な機能がないのでとてもシンプルです。他の利点は・・・特に思いつきません。。
参考
Xamarin.Forms ページ遷移時のデータ受け渡しについて - Qiita
Xamarin.Forms に CallMethodAction が無かったので Behavior で代用してみた - Qiita
OxyPlotでグラフを表示する(WPF)
WPFでグラフを表示するライブラリを探してたとき、Oxyplotを使っている人が多かった(気がした)ので使ってみたメモ。
Webに転がってるサンプルはC#で書かれたものが多いけどせっかく高性能なXAMLが使えるのに使わないのはもったいない!ということでグラフの見た目はなるべくXAMLで書いていきます
開発環境はWIndows10+VisualStudio2015 Community+.NetFramework4.6です。
OxyPlotとは
OXYPlotは様々なグラフの作成を容易にするMIT Licenseのライブラリです。
さまざまなプラットフォームに対応しており
- WPF
- Windows8
- Windows Phone
- Windows Phone Silverlight
- Windows Forms
- Silverlight
- GTK#
- Xwt
- Xamarin.iOS
- Xamarin.Android
- Xamarin.Forms
- Xamarin.Mac
に対応しているらしいです。
ただ、現時点(2016年2月)では プレリリース になっています。
サンプルプロジェクト作成
実際にプロジェクトを作成します。
今回はWPFアプリケーションで作成し、プロジェクト名は「OxyPlotSample」にしました。
Nugetでインストール
NugetからOxyPlotをインストールします。
このとき、「プレリリースを含める」にチェックが入ってないと表示されません。
上のほうになにやらいろいろありますが今回は使わないのでスルーして「OxyPlot.Wpf」をインストールします。
ちなみに私がインストールしたときのバージョンは1.0.0-unstable1983でした。
unstableの文字が怖いのですが安心してください。Nugetにあるやつ全てunstableです。
グラフに表示するデータ作成
無事OxyPlotがインストールできたのでグラフを作成していきたいのですが、その前にグラフ用のデータを作成します。
クラスを追加し、名前を「MainWindowViewModel」にします。一応WPFなのでViewModelと名前がついてますが今回はサンプルなので名前だけです。
中身は以下のようにしました。
using System.Collections.Generic; using OxyPlot; namespace OxyPlotSample { public class MainWindowViewModel { public MainWindowViewModel() { DataList = new List<DataPoint> { {new DataPoint(0, 0)}, {new DataPoint(2, 4)}, {new DataPoint(5, 8)}, {new DataPoint(8, 3)}, {new DataPoint(12, 5)}, }; } public List<DataPoint> DataList { get; } } }
DataPointのコレクションをグラフのItemSourceにバインドしてあげることでグラフが描かれます。
グラフ作成
データもできたので実際にグラフを作成していきます。 今回はもともとあるMainWindow.xamlに直接書いていきます。
<Window x:Class="OxyPlotSample.MainWindow" 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" xmlns:local="clr-namespace:OxyPlotSample" xmlns:oxy="http://oxyplot.org/wpf" mc:Ignorable="d" Title="MainWindow" > <Window.DataContext> <local:MainWindowViewModel /> </Window.DataContext> <Grid> <oxy:Plot> <oxy:Plot.Series > <oxy:LineSeries ItemsSource="{Binding DataList}" /> </oxy:Plot.Series> </oxy:Plot> </Grid> </Window>
基本的にOxyPlotでグラフを描画するときはPlotで作っていきます。
そのなかのSeriesプロパティがグラフの要素(今回は線)を描画する部分です。
ここまで書いてからビルドすると・・・
XAMLのデザイナーでもItemSourceの値通りに描画してくれました!これはありがたい。 実際にデバッグで実行してみても
ちゃんとグラフが描画されてますね!
しかしこのままではなんか殺風景というかせっかく高機能なWPFを使っているのにこれでは面白くありません。 というわけでこれをもとに改造していきます。
背景変更
まず背景の白がなんか安っぽさを出していますよね。黒のほうがかっこいいですよね! というわけで黒にしましょう!
背景を黒にするのは簡単です。PlotのBackgroundプロパティに変更したい色を指定するだけです。
今回は真っ黒なのもなんかあれなので#FF1A1A1A
くらいにしました。
これで実行すると・・・
軸がとても見えにくくなりました(当たり前)
軸変更
なので次は軸を変更していきます。
軸はPlotのAxesに軸クラスを追加していきます。
<oxy:Plot.Axes> <oxy:LinearAxis Position="Left" AxislineColor="Gray" MajorGridlineStyle="Solid" MajorGridlineColor="Gray" MinorGridlineStyle="Dot" MinorGridlineColor="Gray" TicklineColor="Gray" TextColor="Gray" /> </oxy:Plot.Axes>
まずPlot.AxesにLinearAxisを追加します。これは普通の線状の軸です。
まずPositionでどこの軸なのかを明確にします。次にAxislineColorで軸の色を追加します。
MajorGridlineとMinorGridlineは補助線ですね(正式名称知らない)
それぞれのGridlineStyleは線の種類を指定します。Solidは線でDotは点でまんまです。GridlineColorで色を指定します。
TicklineColorは軸の目盛りの色を指定します。TextColorは目盛りの数字の色です。
今回は背景が黒なので軸は灰色で統一しました。
実行結果は・・・
うまくいっているふうに見えますが左軸の色が反映されていません。
ここは結構ハマりました。もっといい解決法があるかもしれませんが、PlotプロパティのPlotAreaBorderColorをTransparent
に、
AxislineStyleをSolid
に変更したら・・・
ちゃんと色がついてくれました!!
どうやらAxislineStyleは最初はNone?だったらしく描画そのものがされていない様子。そしてPlotAreaBorderColorが軸の上に描画されているらしくそっちが優先されているみたいでした。まあ普通は消す必要ないと思いますが今回は軸がちゃんと反映されているか確かめるために消えてもらいました。
グラフの変更
軸を変更できたので次はグラフの線を色々変更していきます。 変更後はこうなりました。
<oxy:Plot.Series> <oxy:LineSeries ItemsSource="{Binding DataList}" LineStyle="Dash" MarkerSize="5" MarkerType="Circle" MarkerStroke="DarkGreen" MarkerStrokeThickness="2" MarkerFill="GreenYellow" /> </oxy:Plot.Series>
あんまり変わっていませんがLineStyleで線の種類を設定します。
Marker関連はItemSourceのコレクションの各要素の値の部分にMarkerTypeで指定した印が入るようになります。
簡単に説明すると、Strokeで印の外枠の色を、StrokeThicknessで外枠の幅を、Fillで印の中の色を指定します。
これの実行結果はこうなります。
凡例作成
グラフの見た目が決まってきたので凡例を作成します。
<oxy:Plot Background="#FF1A1A1A" PlotAreaBorderColor="Transparent" LegendBackground="#FF333333" LegendSymbolLength="30" LegendTextColor="#FFCCCCCC" LegendTitle="グラフの種類" LegendTitleColor="#FFAAAAAA">
凡例はPlotのLegend〜プロパティを指定していきます。 たぶんもう解説しなくてもなんとなく察せると思います。。。
実行結果はこちら
右上端っこにひっそりと凡例が追加されました。
以上が基本的なOxyPlotの使い方というか見た目の設定の仕方になります。
OxyPlotには他にも様々な機能があり、高性能なのですがなかなか解説を探すのに骨が折れます。
また、まだプレリリースなだけあり、結構仕様変更が入ってるぽい?です。(ネットで見つけたコードのプロパティが存在しなかったり)
しかし、比較的楽に、また自由にグラフが描けるのでグラフを描画する必要が出てきたときは使ってみてはいかがでしょうか?
そして、最終的にグラフはこうなりました。
MainWindowViewModel.cs
using System.Collections.Generic; using OxyPlot; namespace OxyPlotSample { public class MainWindowViewModel { public MainWindowViewModel() { DataList = new List<DataPoint> { {new DataPoint(0, 0)}, {new DataPoint(2, 4)}, {new DataPoint(5, 8)}, {new DataPoint(8, 3)}, {new DataPoint(12, 5)}, }; DataList2=new List<DataPoint> { {new DataPoint(0,8) }, {new DataPoint(2,4) }, {new DataPoint(5,6) }, {new DataPoint(7,12) }, {new DataPoint(13,17) }, }; } public List<DataPoint> DataList { get; } public List<DataPoint> DataList2 { get;} } }
MainWindow.xaml
<Window x:Class="OxyPlotSample.MainWindow" 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" xmlns:local="clr-namespace:OxyPlotSample" xmlns:oxy="http://oxyplot.org/wpf" mc:Ignorable="d" Title="MainWindow"> <Window.DataContext> <local:MainWindowViewModel /> </Window.DataContext> <Grid> <oxy:Plot Background="#FF1A1A1A" Padding="20" PlotAreaBorderColor="Transparent" LegendBackground="#66333333" LegendSymbolLength="30" LegendTextColor="#CCCCCCCC" LegendTitle="グラフの種類" LegendTitleColor="#CCAAAAAA" Title="サンプルグラフ" TitleColor="Gray" TitlePadding="20" Subtitle="OxyPlotのサンプルグラフです" TextColor="Gray"> <oxy:Plot.Series> <oxy:AreaSeries ItemsSource="{Binding DataList2}" LineStyle="Solid" Color="Aqua" MarkerType="Square" MarkerFill="Blue" MarkerStroke="DarkBlue" Title="サンプルエリアグラフ" /> <oxy:LinearBarSeries ItemsSource="{Binding DataList}" StrokeColor="OrangeRed" BarWidth="30" FillColor="DarkOrange" Title="サンプル棒グラフ" /> <oxy:LineSeries ItemsSource="{Binding DataList}" BrokenLineStyle="LongDash" LineStyle="Dash" MarkerSize="5" MarkerType="Circle" MarkerStroke="DarkGreen" MarkerStrokeThickness="2" MarkerFill="GreenYellow" Title="サンプル線グラフ" /> </oxy:Plot.Series> <oxy:Plot.Axes> <oxy:LinearAxis Position="Left" AxislineColor="Gray" AxislineStyle="Solid" MajorGridlineStyle="Solid" MajorGridlineColor="Gray" MinorGridlineStyle="Dot" MinorGridlineColor="Gray" TextColor="Gray" TickStyle="None" StartPosition="-0.01" Maximum="20" IntervalLength="150" Title="X軸" TitleColor="Gray" /> <oxy:LinearAxis Position="Bottom" AxislineColor="Gray" AxislineStyle="Solid" MajorGridlineStyle="Solid" MajorGridlineColor="Gray" MinorGridlineStyle="Dot" MinorGridlineColor="Gray" TextColor="Gray" TickStyle="None" Maximum="15" IntervalLength="200" StartPosition="-0.01" Title="Y軸" TitleColor="Gray" /> </oxy:Plot.Axes> </oxy:Plot> </Grid> </Window>
AreaSeriesってどういうとき使うの?
以上です。