弱いイベントパターンを用いたリスナー登録機能を持つビューモデルベースを使った簡単なプログラム

WeakEventViewModelBase クラスの実装の見直しを反映しました(2020年5月27日)。

前回の投稿で弱いイベントパターンを用いたリスナー登録機能を持つビューモデルベースを作ったので、それを使った1ウィンドウの簡単なプログラムを作りました。1ウィンドウなので、弱いイベントパターンを使う意味はあまりないのですけど、まぁ使い方の簡単な例ということで 😉 簡単なとはいえ、ビューモデルへプロパティ変更通知を行うサービス層が必要なので、構成はそれなりになります 😀

ウィンドウはこんな感じです。

MainWindow
MainWindow

ボタンをクリックすると固定値のデータが追加され、追加されたデータが一覧に追加表示されます。ビューモデル上の追加処理ではサービス層へのデータの追加依頼だけを行なっていて、サービス層で DAL 層へのデータ追加依頼とビューモデルへのプロパティ変更通知を行い、変更通知を受け取ったビューモデルが一覧のプロパティ変更通知をビューへ行うことで、ウィンドウ上にデータ追加が反映される仕組みになります。

まず、NuGet から MakViewModelBase パッケージを導入します(ソリューション エクスプローラーからプロジェクトを右クリックして、NuGet パッケージの管理を選択し、「参照」をクリックして「Mak」で検索すると MakCraft.MakViewModelBase パッケージが表示されるので、選択してインストールボタンをクリックします)。

準備ができたら、モデル層から。プロジェクトに Models フォルダを作成し、Book クラスを作ります。

namespace WeakEventViewModelBaseTestApp.Models
{
    class Book
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Author { get; set; }
        public decimal Price { get; set; }
        public string Feature { get; set; }
    }
}

次にデータアクセスレイヤ層。プロジェクトに DAL フォルダを作成し、IBookRepository インターフェイスと BookRepository クラスを作ります。今回データの永続化は行わないので、データはリストで保持するのみで外部とのやり取りはありません。

IBookRepository インターフェイス

using System.Linq;

using WeakEventViewModelBaseTestApp.Models;

namespace WeakEventViewModelBaseTestApp.DAL
{
    interface IBookRepository
    {
        int Count { get; }

        IQueryable<Book> FindBooks();
        Book GetBook(int id);

        Book Add(Book book);
        void Update(Book book);
        void Delete(int id);
    }
}

BookRepository クラス

using System;
using System.Collections.Generic;
using System.Linq;

using WeakEventViewModelBaseTestApp.Models;

namespace WeakEventViewModelBaseTestApp.DAL
{
    class BookRepository : IBookRepository
    {
        public BookRepository()
        {
            booksInit();
        }

        private List<Book> _books;

        public int Count
        {
            get { return _books.Count; }
        }

        public IQueryable<Book> FindBooks()
        {
            return _books.AsQueryable();
        }

        public Book GetBook(int id)
        {
            return _books.Find(w => w.Id == id);
        }

        public Book Add(Book book)
        {
            var newId = _books.Max(w => w.Id);
            if (newId == int.MaxValue)
            {
                throw new OverflowException("Book の ID が 許容される最大値に達しているため追加できませんでした");
            }
            book.Id = ++newId;
            _books.Add(book);

            return book;
        }

        public void Update(Book book)
        {
            var index = _books.FindIndex(w => w.Id == book.Id);
            if (index < 0)
            {
                throw new ArgumentException("更新対象のキーを持つデータは存在しません。");
            }
            _books[index] = book;
        }

        public void Delete(int id)
        {
            var target = _books.Find(w => w.Id == id);
            _books.Remove(target);
        }

        private void booksInit()
        {
            _books = new List<Book> {
                new Book {
                    Id = 0,
                    Title = "猫の本",
                    Author = "三毛猫",
                    Price = 1200,
                    Feature = "三毛猫の日常を描いた問題作"
                },
                new Book {
                    Id = 1,
                    Title = "犬の本",
                    Author = "スピッツ",
                    Price = 1400,
                    Feature = "スピッツの日常を描いた話題作"
                },
            };
        }
    }
}

次にプロジェクトに Services フォルダを作成し、IBookService インターフェイスと BookService クラスを作ります。

IBookService インターフェイス
プロパティ変更通知を実装する必要があるので、INotifyPropertyChanged インターフィスを持つことを明示しています。プロパティ変更通知を一覧取得機能に対して行うために、List<Book> を返す Books をメソッドではなくプロパティにしています。

using System.Collections.Generic;
using System.ComponentModel;

using WeakEventViewModelBaseTestApp.Models;

namespace WeakEventViewModelBaseTestApp.Services
{
    interface IBookService : INotifyPropertyChanged
    {
        int Count { get; }

        List<Book> Books { get; }

        Book GetBook(int id);

        void AddBook(Book book);
        void UpdateBook(Book book);
        void DeleteBook(int id);
    }
}

BookService クラス
プロパティ変更通知機能を実装した NotifyObject から派生させ、IBookService インターフィスを実装します。プロパティ変更通知は Books プロパティにだけ行なっています(件数変更を伴わないのはデータ更新のみであることから簡略化)。

using System.Collections.Generic;
using System.Linq;

using MakCraft.ViewModels;

using WeakEventViewModelBaseTestApp.DAL;
using WeakEventViewModelBaseTestApp.Models;

namespace WeakEventViewModelBaseTestApp.Services
{
    class BookService : NotifyObject, IBookService
    {
        private IBookRepository _repository;

        public BookService() : this(new BookRepository()) { }
        public BookService(IBookRepository repository)
        {
            _repository = repository;
        }

        public int Count
        {
            get { return _repository.Count; }
        }

        public List<Book> Books
        {
            get { return _repository.FindBooks().ToList(); }
        }

        public Book GetBook(int id)
        {
            return _repository.GetBook(id);
        }

        public void AddBook(Models.Book book)
        {
            _repository.Add(book);

            base.RaisePropertyChanged(() => Books);
        }

        public void UpdateBook(Models.Book book)
        {
            _repository.Update(book);

            base.RaisePropertyChanged(() => Books);
        }

        public void DeleteBook(int id)
        {
            _repository.Delete(id);

            base.RaisePropertyChanged(() => Books);
        }
    }
}

次にプロジェクトに ViewModels フォルダを作成し、MainWindowViewModel クラスを作ります。

MainWindowViewModel クラス
MainWindowViewModel クラスは弱いイベントパターンを用いたリスナー登録機能を持つビューモデルベース WeakEventViewModelBase クラスから派生させます。
コンストラクタ内で PropertyChangedEventManager.AddListener メソッドでサービス層(イベントソース)と MainWindowViewModel のインスタンス、プロパティ名(string.Empty ですべてを設定)を組みで登録しています。
protected override void OnReceivedPropertyChangeNotification(Type managerType, object sender, EventArgs e) メソッドで、イベントハンドラを設定しています。ビューに対して BooksCount プロパティと Books プロパティのプロパティ変更通知を行ないます(サービス層でのプロパティ変更通知を Books プロパティのみとしているため、ひとくくりでの通知になります)。


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Input;

using MakCraft.ViewModels;

using WeakEventTest.Models;
using WeakEventTest.Services;

namespace WeakEventTest.ViewModels
{
    class MainWindowViewModel : WeakEventViewModelBase
    {
        private IBookService _service;

        public MainWindowViewModel() : this(new BookService()) { }
        public MainWindowViewModel(IBookService service)
        {
            _service = service;
            PropertyChangedEventManager.AddListener(_service, this, string.Empty);
        }

        public int BooksCount
        {
            get { return _service.Count; }
        }

        public List<Book> Books
        {
            get { return _service.Books; }
        }

        private void addBookCommandExecute()
        {
            var book = new Book
            {
                Id = 0,
                Title = "くまの本",
                Author = "森のくまさん",
                Price = 400,
                Feature = "森のくまさんの生活を描いた自信作"
            };
            _service.AddBook(book);
        }
        private ICommand addBookCommand;
        public ICommand AddBookCommand
        {
            get
            {
                if (addBookCommand == null)
                    addBookCommand = new RelayCommand(addBookCommandExecute);
                return addBookCommand;
            }
        }

        protected override void OnReceivedPropertyChangeNotification(Type managerType, object sender, EventArgs e)
        {
            // PropertyChangedEventManager からのイベント通知であることを確認
            if (managerType != typeof(PropertyChangedEventManager))
            {
                return;
            }

            // イベントソースが IBookService であることを確認
            if (!(sender is IBookService service))
            {
                return;
            }

            // PropertyChangedEventArgs であることを確認
            if (!(e is PropertyChangedEventArgs eventArgs))
            {
                return;
            }

            switch (eventArgs.PropertyName)
            {
                case nameof(IBookService.Books):
                    base.RaisePropertyChanged(() => Books);
                    base.RaisePropertyChanged(() => BooksCount);
                    break;
            }
        }
    }
}

最後に MainWindow.xaml です。

<Window x:Class="WeakEventViewModelBaseTestApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:WeakEventViewModelBaseTestApp.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>

    <StackPanel>
        <TextBlock Text="弱いイベント・パターンを利用したサービスからのプロパティ変更通知のテスト"
                   HorizontalAlignment="Center" Margin="10"/>
        <StackPanel Margin="10">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
                <Label Content="データ件数:" />
                <TextBlock Text="{Binding BooksCount}" VerticalAlignment="Center" MinWidth="20" />
            </StackPanel>
            <DataGrid Name="BooksGrid" ItemsSource="{Binding Books}" AutoGenerateColumns="False"
                      IsReadOnly="True" HorizontalAlignment="Center">
                <DataGrid.Columns>
                    <DataGridTextColumn Binding="{Binding Title}" Header="書名" />
                    <DataGridTextColumn Binding="{Binding Author}" Header="著者" />
                    <DataGridTextColumn Binding="{Binding Price, StringFormat={}{0:N0}円}" Header="価格" />
                    <DataGridTextColumn Binding="{Binding Feature}" Header="特色" />
                </DataGrid.Columns>
            </DataGrid>
            <Button Content="追加" Padding="10 6" Command="{Binding AddBookCommand}"
                    Margin="10" Width="{Binding ElementName=BooksGrid, Path=ActualWidth}"/>
        </StackPanel>
    </StackPanel>
</Window>

以上でプログラムの完成です。次回は複数ウィンドウにして、弱いイベントパターンの適用による、ビューモデル(イベントリスナ)の破棄の確認も行います。


弱いイベントパターンを用いたリスナー登録機能を持つビューモデルベースを使った簡単なプログラム」への2件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です