Livetのサンプルを復元してみる

[Livet](http://ugaya40.hateblo.jp/entry/livet)のサンプルプロジェクトが行方不明なので[Livet を使ってみた (by VB)](http://d.hatena.ne.jp/hilapon/20110720/1311162979)をC#に逆移植してみた。

[コード](https://github.com/ousttrue/WpfSample/tree/master/LivetSample)

# 更新
* CompositeDisposableを追加
* Xamlも追加

# 新規プロジェクト
LivetSampleを作成。nugetでLivetをインストール。

# MainWindowViewModel
ViewModelを作成。

```CSharp:ViewModel
    class MainWindowViewModel: Livet.ViewModel
    {
    }
```

DataContextにセット。

```xml:ネームスペースi,ei,lを追加してDataContextにViewModelをセット。
<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<Member>を供給するのが役割となる。
MVVM的には、ObservableCollection<INotifyPropertyChanged>がモデルであると。
特に注意を払うところはない。

## Model

```csharp:MemberとMain

    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なんてあるんですな。

```csharp:ItemのViewModel
    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がポイントか。

```csharp:ViewModel
    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
の準備として必要なようだ。

```cshrp:App
    /// <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

```xml:MainWindow.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>
```

```xml:DetailWindow.xaml
<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>
```

以上でした。