LoginSignup
24
19

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-02-19

Livetのサンプルプロジェクトが行方不明なのでLivet を使ってみた (by VB)をC#に逆移植してみた。

コード

更新

  • CompositeDisposableを追加
  • Xamlも追加

新規プロジェクト

LivetSampleを作成。nugetでLivetをインストール。

MainWindowViewModel

ViewModelを作成。

ViewModel
    class MainWindowViewModel: Livet.ViewModel
    {
    }

DataContextにセット。

ネームスペース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を供給するのが役割となる。
MVVM的には、ObservableCollectionがモデルであると。
特に注意を払うところはない。

Model

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

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がポイントか。

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

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

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>
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>

以上でした。

24
19
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
19