Livetのサンプルプロジェクトが行方不明なのでLivet を使ってみた (by VB)をC#に逆移植してみた。
更新
- CompositeDisposableを追加
- Xamlも追加
新規プロジェクト
LivetSampleを作成。nugetでLivetをインストール。
MainWindowViewModel
ViewModelを作成。
class MainWindowViewModel: Livet.ViewModel
{
}
DataContextにセット。
<Window x:Class="LivetSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
xmlns:local="clr-namespace:LivetSample"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
</Grid>
</Window>
移植
Modelから。ObservableCollectionを供給するのが役割となる。
MVVM的には、ObservableCollectionがモデルであると。
特に注意を払うところはない。
Model
class Member : Livet.NotificationObject
{
Main m_parent;
String m_name;
public String Name
{
get { return m_name; }
set
{
if (m_name == value) return;
m_name = value;
RaisePropertyChanged("Name");
}
}
DateTime m_birthday;
public DateTime Birthday
{
get { return m_birthday; }
set
{
if (m_birthday == value) return;
m_birthday = value;
RaisePropertyChanged("BirthDay");
}
}
String m_memo;
public String Memo
{
get { return m_memo; }
set
{
if (m_memo == value) return;
m_memo = value;
RaisePropertyChanged("Memo");
}
}
public Member(Main parent)
{
m_parent = parent;
}
public bool IsIncludedInMainCollection()
{
return m_parent.Members.Contains(this);
}
public void AddThisToMainCollection()
{
m_parent.Members.Add(this);
}
public void RemoveThisFromMainCollection()
{
m_parent.Members.Remove(this);
}
}
class Main : Livet.NotificationObject
{
ObservableCollection<Member> m_members;
public ObservableCollection<Member> Members
{
get
{
if (m_members == null)
{
m_members = new ObservableCollection<Member>
{
new Member(this){ Name="hoge", Birthday=new DateTime(1988, 12, 12), Memo="HogeHoge" },
new Member(this){ Name="fuga", Birthday=new DateTime(1999, 11, 11), Memo="FugaFuga" },
};
// 2015/3/12追記。Disposeで解放されるようにする
CompositeDisposable.Add(m_members);
}
return m_members;
}
}
}
ItemのViewModel
WeakEvent登場。良く知らないが重要らしい。
ViewModelCommand.RaiseCanExecuteChangedが地味に重要。
IDataErrorInfoなんてあるんですな。
class MemberViewModel : Livet.ViewModel, IDataErrorInfo
{
Member m_member;
MainWindowViewModel m_parent;
// 弱参照?
Livet.EventListeners.WeakEvents.PropertyChangedWeakEventListener m_weak;
Livet.Commands.ViewModelCommand m_removeCommand;
public ICommand RemoveCommand
{
get
{
if (m_removeCommand == null)
{
m_removeCommand = new Livet.Commands.ViewModelCommand(() =>
{
m_member.RemoveThisFromMainCollection();
});
}
return m_removeCommand;
}
}
public MemberViewModel(Member member, MainWindowViewModel parent)
{
m_member = member;
m_parent = parent;
InitializeInput();
// こうか?
m_weak = new Livet.EventListeners.WeakEvents.PropertyChangedWeakEventListener(member,
(o, e) =>
{
RaisePropertyChanged(e.PropertyName);
if (e.PropertyName == "Birthday")
{
RaisePropertyChanged("Age");
}
});
// 2015/3/12追記。Disposeで解放されるようにする
CompositeDisposable.Add(m_weak);
}
public String Name
{
get { return m_member.Name; }
set { m_member.Name = value; }
}
public DateTime Birthday
{
get { return m_member.Birthday; }
set { m_member.Birthday = value; }
}
public String Memo
{
get { return m_member.Memo; }
set { m_member.Memo = value; }
}
public Int32 Age
{
get { return (DateTime.Now - Birthday).Days / 365; }
}
bool m_isChecked;
public bool IsChecked
{
get { return m_isChecked; }
set
{
if (m_isChecked == value) return;
m_isChecked = value;
RaisePropertyChanged("IsChecked");
}
}
#region Input
String m_inputName;
public String InputName
{
get { return m_inputName; }
set
{
if (m_inputName == value) return;
m_inputName = value;
if (value==null || String.IsNullOrEmpty(value.Trim()))
{
m_errors["InputName"] = "名前は必須です";
}
else
{
m_errors["InputName"] = null;
}
RaisePropertyChanged("Error");
}
}
String m_inputBirthday;
public String InputBirthday
{
get{return m_inputBirthday;}
set{
if(m_inputBirthday==value)return;
m_inputBirthday=value;
DateTime inputDateTime;
if(String.IsNullOrEmpty(m_inputBirthday)){
m_errors["InputBirthday"] = "生年月日は必須です";
}
else if(!DateTime.TryParse(m_inputBirthday, out inputDateTime)){
m_errors["InputBirthday"] = "年月日として不正な形式です";
}
else if(inputDateTime > DateTime.Now){
m_errors["InputBirthday"] = "未来の日付は指定できません";
}
else{
m_errors["InputBirthday"] = null;
}
RaisePropertyChanged("Error");
}
}
public String InputMemo{get;set;}
void InitializeInput()
{
InputName = m_member.Name;
if (m_member.Birthday != DateTime.MinValue)
{
InputBirthday = m_member.Birthday.ToString("yyyy/MM/dd");
}
InputMemo = m_member.Memo;
m_errors.Clear();
}
#endregion
Livet.Commands.ViewModelCommand m_saveCommand;
public ICommand SaveCommand
{
get
{
if (m_saveCommand == null)
{
m_saveCommand = new Livet.Commands.ViewModelCommand(() =>
{
Name = InputName;
Birthday = DateTime.Parse(InputBirthday);
Memo = InputMemo;
if (!m_member.IsIncludedInMainCollection())
{
m_member.AddThisToMainCollection();
}
// Viewに画面遷移用メッセージを送信しています。
// Viewは対応するメッセージキーを持つInteractionTransitionMessageTriggerでこのメッセージを受信します。
Messenger.Raise(new Livet.Messaging.Windows.WindowActionMessage(Livet.Messaging.Windows.WindowAction.Close, "Close"));
},
() =>
{
if(!String.IsNullOrEmpty(Error)){
return false;
}
if(String.IsNullOrEmpty(InputName)){
return false;
}
if(String.IsNullOrEmpty(InputBirthday)){
return false;
}
return true;
});
// CanExecuteの更新
PropertyChanged += (o, e) =>
{
if (e.PropertyName == "Error")
{
m_saveCommand.RaiseCanExecuteChanged();
}
};
}
return m_saveCommand;
}
}
Livet.Commands.ViewModelCommand m_cancelCommand;
public ICommand CancelCommand
{
get
{
if (m_cancelCommand == null)
{
m_cancelCommand = new Livet.Commands.ViewModelCommand(() =>
{
// 入力情報初期化
InitializeInput();
// Viewに画面遷移用メッセージを送信しています。
// Viewは対応するメッセージキーを持つInteractionTransitionMessageTriggerでこのメッセージを受信します。
Messenger.Raise(new WindowActionMessage(WindowAction.Close, "Close"));
});
}
return m_cancelCommand;
}
}
#region IDataErrorInfo
Dictionary<String, String> m_errors = new Dictionary<string, string>
{
{"InputName", null}, {"InputBirthday", null}
};
public string Error
{
get
{
var list = new List<String>();
if (!String.IsNullOrEmpty(this["InputName"]))
{
list.Add("名前");
}
if (!String.IsNullOrEmpty(this["InputBirthday"]))
{
list.Add("生年月日");
}
if (!list.Any())
{
return null;
}
return String.Join("・", list) + "が不正です";
}
}
public string this[string columnName]
{
get
{
if (m_errors.ContainsKey(columnName))
{
return m_errors[columnName];
}
else
{
return null;
}
}
}
#endregion
}
ViewModel
Livet.ViewModelHelper.CreateReadOnlyDispatcherCollectionがポイントか。
class MainWindowViewModel: Livet.ViewModel
{
Main m_model;
public Main Model
{
get
{
if (m_model == null)
{
m_model = new Main();
}
return m_model;
}
}
Livet.ReadOnlyDispatcherCollection<MemberViewModel> m_members;
public Livet.ReadOnlyDispatcherCollection<MemberViewModel> Members
{
get{
if(m_members==null){
m_members = Livet.ViewModelHelper.CreateReadOnlyDispatcherCollection(Model.Members
, m =>new MemberViewModel(m, this)
, Livet.DispatcherHelper.UIDispatcher
);
}
return m_members;
}
}
Livet.Commands.ViewModelCommand m_editNewCommand;
public ICommand EditNewCommand
{
get
{
if (m_editNewCommand == null)
{
m_editNewCommand = new Livet.Commands.ViewModelCommand(() =>
{
Messenger.Raise(new Livet.Messaging.TransitionMessage(
new MemberViewModel(new Member(m_model), this), "Transition"));
});
}
return m_editNewCommand;
}
}
Livet.Commands.ViewModelCommand m_removeCommand;
public ICommand RemoveCommand
{
get
{
if(m_removeCommand==null)
{
m_removeCommand = new Livet.Commands.ViewModelCommand(() =>
{
foreach(var m in Members.Where(m=>m.IsChecked).ToArray())
{
m.RemoveCommand.Execute(null);
}
});
}
return m_removeCommand;
}
}
}
###NullReferenceException
ObservableCollection.Removeしたときに発生するときがある。
http://ts7u.blogspot.jp/2014/02/livetcommandsviewmodelcommandraisecanex.html
以下のコードが
Livet.ViewModelHelper.CreateReadOnlyDispatcherCollection
の準備として必要なようだ。
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Livet.DispatcherHelper.UIDispatcher = this.Dispatcher;
}
}
Xaml
<Window x:Class="LivetSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
xmlns:local="clr-namespace:LivetSample"
Title="メンバー管理" Height="350" Width="525"
>
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<i:Interaction.Triggers>
<!--ViewからのTransitionというメッセージキーを持つメッセージを受信します-->
<!--TransitionInteractionMessageAction で画面遷移を行っています-->
<l:InteractionMessageTrigger MessageKey="Transition" Messenger="{Binding Messenger}">
<l:TransitionInteractionMessageAction WindowType="{x:Type local:DetailWindow}" Mode="Modal"/>
</l:InteractionMessageTrigger>
</i:Interaction.Triggers>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" FontSize="17">メンバー管理</TextBlock>
<Button Grid.Column="2" Command="{Binding EditNewCommand}">追加</Button>
<!--
DelegateCommand.LatestCanExecuteResultプロパティは最新のCanExecuteの結果をboolで保持します。
コントロールのCommandプロパティを使用しない場合の、コマンドの実行可否状態によるコントロールの制御に使用します。
-->
<Button Grid.Column="3" Content="削除" IsEnabled="{Binding RemoveCommand.LatestCanExecuteResult}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<l:ConfirmationDialogInteractionMessageAction>
<!--
DirectInteractionMessageのCallbackCommandプロパティにコマンドを設定する事で
Viewで生成したメッセージを元にアクション実行後、コマンドを実行させる事ができます。
その場合、コマンドには引数としてメッセージが渡ります
-->
<l:DirectInteractionMessage CallbackCommand="{Binding RemoveCommand}">
<l:ConfirmationMessage Button="OKCancel"
Caption="確認"
Text="本当にチェックの付けられたメンバー情報を削除しますか?"
Image="Information"/>
</l:DirectInteractionMessage>
</l:ConfirmationDialogInteractionMessageAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<!--ViewModelを経由せずにメッセージを生成し、Windowを閉じています-->
<!--Livetでは、ViewModelを経由する必要のない相互作用をこの様にView内で完結させられます-->
<Button Grid.Column="4" Content="終了">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<l:WindowInteractionMessageAction>
<l:DirectInteractionMessage>
<l:WindowActionMessage Action="Close"/>
</l:DirectInteractionMessage>
</l:WindowInteractionMessageAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
<ListView Grid.Row="1" ItemsSource="{Binding Members}">
<ListView.View>
<GridView>
<GridViewColumn Width="30">
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsChecked}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="名前" Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="年齢" Width="40">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Age}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="生年月日" Width="85">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Birthday,StringFormat=yyyy/MM/dd}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="備考" Width="170">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Memo}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="65">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Width="50" Content="変更">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<!--Viewから詳細ウィンドウを表示させています。ViewModelを経由させていません-->
<l:TransitionInteractionMessageAction Mode="Modal" WindowType="{x:Type local:DetailWindow}">
<l:DirectInteractionMessage>
<l:TransitionMessage TransitionViewModel="{Binding}"/>
</l:DirectInteractionMessage>
</l:TransitionInteractionMessageAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
<Window x:Class="LivetSample.DetailWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
Title="メンバー詳細" Height="300" Width="300"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="{x:Type TextBox}">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Margin" Value="5"/>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<!--Closeというメッセージキーを持つメッセージがViewModelから届いた際に起動するトリガーです-->
<i:Interaction.Triggers>
<l:InteractionMessageTrigger MessageKey="Close" Messenger="{Binding Messenger}">
<l:WindowInteractionMessageAction/>
</l:InteractionMessageTrigger>
</i:Interaction.Triggers>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0">名前:</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0">誕生日:</TextBlock>
<TextBlock Grid.Row="2" Grid.Column="0">備考:</TextBlock>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding InputName,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding InputBirthday,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding InputMemo,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Row="3" Grid.ColumnSpan="2" TextWrapping="Wrap" FontSize="16" Foreground="Red" FontWeight="Bold" Text="{Binding Error}" HorizontalAlignment="Center"/>
<StackPanel Height="30" Grid.Row="4" Grid.ColumnSpan="2" Margin="5" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Width="70" Command="{Binding SaveCommand}">確定</Button>
<Button Width="70" Command="{Binding CancelCommand}">キャンセル</Button>
</StackPanel>
</Grid>
</Window>
以上でした。