ValidationViewModelBase と DateTimePicker を利用したアプリケーション

前々回と前回で書いた ValidationViewModelBaseDateTimePicker を利用して、会議室予約システムを作ってみます(ただし、あくまで利用例なので、予定の更新、削除、予定一覧の絞込みやページング表示、DB へのデータ保存などの機能は作りこみません 😉 )。

画面はこんな感じです。

会議室一覧

「GC 実行」ボタンは、子ウィンドウが破棄されているかの確認用です。
デバッグメッセージで確認します。

会議室予約画面
希望時間に先約があったとき

ソースコード一式と動作確認用の WPF プロジェクト(Visual Studio Express 2012 for Windows Desktop で作成しています。)の zip ファイルをダウンロードできます。

ユーザーコントロール「DateTimePicker」の組み込みまではできている前提で書きます。

まずはモデルから。プロジェクトに「Models」フォルダを作成します。

Models フォルダに「ConferenceRoom」クラスを作成します。

namespace ValidationTestRoomReservation.Models
{
    public class ConferenceRoom
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Capacity { get; set; }
    }
}

次に Models フォルダに「Reservation」クラスを作成します。

using System;

namespace ValidationTestRoomReservation.Models
{
    public class Reservation
    {
        public int RoomId { get; set; }
        public DateTime Start { get; set; }
        public DateTime End { get; set; }
        public string SubscriberName { get; set; }
    }
}

次に、プロジェクトに「Repositories」フォルダを作成します。
Repositories フォルダに「IRoomRepository」インターフェイスを作成します。

using System.Linq;
using ValidationTestRoomReservation.Models;

namespace ValidationTestRoomReservation.Repositories
{
    public interface IRoomRepository
    {
        IQueryable<ConferenceRoom> FindRoom();
        ConferenceRoom GetRoom(int id);

        IQueryable<Reservation> GetReservationList(int roomId);
        void AddReservation(Reservation reservation);
    }
}

Repositories フォルダに「RoomRepository」クラスを作成します(ここで動作確認用のデータもセットしています)。

using System;
using System.Collections.Generic;
using System.Linq;
using ValidationTestRoomReservation.Models;

namespace ValidationTestRoomReservation.Repositories
{
    public class RoomRepository : IRoomRepository
    {
        private List<ConferenceRoom> _room;
        private List<Reservation> _reservation;

        public RoomRepository()
        {
            _room = new List<ConferenceRoom>
            {
                new ConferenceRoom
                {
                    Id = 0,
                    Name = "第一会議室",
                    Capacity = 10,
                },
                new ConferenceRoom
                {
                    Id = 1,
                    Name = "第二会議室",
                    Capacity = 20,
                },
                new ConferenceRoom
                {
                    Id = 2,
                    Name = "第三会議室",
                    Capacity = 30,
                },
                new ConferenceRoom
                {
                    Id = 4,
                    Name = "大ホール",
                    Capacity = 1500,
                },
            };
            _reservation = new List<Reservation>
            {
                new Reservation
                {
                    RoomId = 1,
                    Start = DateTime.Parse(DateTime.Now.ToString("yyyy/MM/dd") + " 10:00:00"),
                    End = DateTime.Parse(DateTime.Now.ToString("yyyy/MM/dd") + " 11:30:00"),
                    SubscriberName = "ほげ",
                },
                new Reservation
                {
                    RoomId = 1,
                    Start = DateTime.Parse(DateTime.Now.ToString("yyyy/MM/dd") + " 13:00:00"),
                    End = DateTime.Parse(DateTime.Now.ToString("yyyy/MM/dd") + " 14:30:00"),
                    SubscriberName = "ほげ",
                },
            };
        }

        public IQueryable<ConferenceRoom> FindRoom()
        {
            return _room.AsQueryable();
        }

        public ConferenceRoom GetRoom(int id)
        {
            return _room.Find(w => w.Id == id);
        }

        public IQueryable<Reservation> GetReservationList(int roomId)
        {
            return _reservation
                .AsQueryable()
                .Where(w => w.RoomId == roomId);
        }

        public void AddReservation(Reservation reservation)
        {
            _reservation.Add(reservation);
        }
    }
}

次にプロジェクトに「Services」フォルダを作成します。
業務ロジックでの検証エラーチェックを行い、エラーの有無(エラーがあった場合はエラー種別)を返し、エラーメッセージの登録は ViewModel で行うようにしています。
Services フォルダに「IReserveService」インターフェイスを作成します。

using System;
using System.Linq;
using ValidationTestRoomReservation.Models;

namespace ValidationTestRoomReservation.Services
{
    public interface IReserveService
    {
        IQueryable<ConferenceRoom> GetRooms();

        IsReservError ReserveRoom(int roomId, DateTime start, DateTime end, string subscriverName);

        IQueryable<Reservation> GetReservations(int roomId);

        bool IsReservValid(DateTime start, DateTime end);
    }

    /// <summary>
    /// エラーの有無及びエラー発生の場所を表します。
    /// </summary>
    public enum IsReservError
    {
        None, StartTime, EndTime, BothTime, Repository
    }
}

Services フォルダに「ReserveService」クラスを作成します。

using System;
using System.Linq;
using ValidationTestRoomReservation.Models;
using ValidationTestRoomReservation.Repositories;

namespace ValidationTestRoomReservation.Services
{
    public class ReserveService : IReserveService
    {
        private IRoomRepository _repository;

        public ReserveService() : this(new RoomRepository()) { }
        public ReserveService(IRoomRepository repository)
        {
            _repository = repository;
        }

        public IQueryable<ConferenceRoom> GetRooms()
        {
            return _repository.FindRoom();
        }

        public IsReservError ReserveRoom(int roomId, DateTime start, DateTime end, string subscriverName)
        {
            if (isReserved(roomId, start, end))
            {
                var conflict = _repository.GetReservationList(roomId)
                    .Where(w => w.Start <= end && start <= w.End)
                    .ToList();
                if (conflict.Where(w => start < w.Start).Count() != 0)
                {
                    return IsReservError.EndTime;
                }
                else if (conflict.Where(w => w.End < end).Count() != 0)
                {
                    return IsReservError.StartTime;
                }
                else
                {
                    return IsReservError.BothTime;
                }
            }

            try
            {
                _repository.AddReservation(new Reservation
                {
                    RoomId = roomId,
                    Start = start,
                    End = end,
                    SubscriberName = subscriverName,
                });
            }
            catch (Exception)
            {
                return IsReservError.Repository;
            }
            return IsReservError.None;
        }

        public IQueryable<Reservation> GetReservations(int roomId)
        {
            return _repository.GetReservationList(roomId);
        }

        public bool IsReservValid(DateTime start, DateTime end)
        {
            return (start < end);
        }

        private bool isReserved(int roomId, DateTime start, DateTime end)
        {
            return (_repository.GetReservationList(roomId)
                .Where(w => w.End >= start && w.Start <= end)
                .Count() != 0);
        }
    }
}

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using ValidationTestRoomReservation.Models;
using ValidationTestRoomReservation.Services;
using MakCraft.ViewModels;

namespace ValidationTestRoomReservation.ViewModels
{
    public class MainWindowViewModel : ValidationViewModelBase
    {
        private IReserveService _service;
        private int _selectedRow;

        public MainWindowViewModel()
        {
            _service = new ReserveService();
        }
        public MainWindowViewModel(IReserveService service)
        {
            _service = service;
        }

        public IList<ConferenceRoom> Rooms
        {
            get
            { return _service.GetRooms().ToList(); }
        }

        public int SelectedRow
        {
            get { return _selectedRow; }
            set
            {
                _selectedRow = value;
                base.RaisePropertyChanged(PropertyHelper.GetName(() => SelectedRow));
            }
        }

        private void showRoomCommandExecute()
        {
            System.Diagnostics.Debug.WriteLine("showButton clicked! {0}", SelectedRow);
            var reserveWindow = new ReserveWindow();
            (reserveWindow.DataContext as ReserveWindowViewModel).RoomId = Rooms[SelectedRow].Id;
            var dialogResult = reserveWindow.ShowDialog();
            System.Diagnostics.Debug.WriteLine("ReserveWindows Closed!");
            reserveWindow = null;
        }
        private bool showRoomCommandCanExecute
        {
            get { return (SelectedRow > -1); }
        }
        private ICommand showRoomCommand;
        public ICommand ShowRoomCommand
        {
            get
            {
                if (showRoomCommand == null)
                    showRoomCommand = new RelayCommand(
                        param => showRoomCommandExecute(),
                        param => showRoomCommandCanExecute
                    );
                return showRoomCommand;
            }
        }

        private void gcCommandExecute()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
        private ICommand gcCommand;
        public ICommand GcCommand
        {
            get
            {
                if (gcCommand == null)
                    gcCommand = new RelayCommand(
                        param => gcCommandExecute()
                    );
                return gcCommand;
            }
        }

    }
}

ViewModels フォルダに「ReserveWindowViewModel」クラスを作成します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using System.ComponentModel.DataAnnotations;
using ValidationTestRoomReservation.Models;
using ValidationTestRoomReservation.Services;
using MakCraft.ViewModels;

namespace ValidationTestRoomReservation.ViewModels
{
    public class ReserveWindowViewModel : ValidationViewModelBase
    {
        private IReserveService _service;
        private int _roomId;
        private DateTime _start;
        private DateTime _end;
        private string _subscriber;
        private string _logicalError;
        public ReserveWindowViewModel()
        {
            _service = new ReserveService();
            System.Diagnostics.Debug.WriteLine("ReserveWindowViewModel created!");
            Start = DateTime.Now;
            End = DateTime.Now;
        }
        public ReserveWindowViewModel(IReserveService service)
        {
            _service = service;
        }

        public int RoomId
        {
            get { return _roomId; }
            set
            {
                _roomId = value;
                RaisePropertyChanged(PropertyHelper.GetName(() => RoomName));
                RaisePropertyChanged(PropertyHelper.GetName(() => Reservations));
            }
        }

        public string RoomName
        {
            get { return _service.GetRooms().Where(w => w.Id == RoomId).First().Name; }
        }

        public IList<Reservation> Reservations
        {
            get { return _service.GetReservations(RoomId).OrderBy(w => w.Start).ToList(); }
        }

        public DateTime Start
        {
            get { return _start; }
            set
            {
                var propertyName = PropertyHelper.GetName(() => Start);
                base.RemoveItemValidationError(propertyName);
                _start = value;
                logiclCheck();
                base.RaisePropertyChanged(PropertyHelper.GetName(() => Start));
            }
        }

        public DateTime End
        {
            get {return _end;}
            set
            {
                var propertyName = PropertyHelper.GetName(() => End);
                base.RemoveItemValidationError(propertyName);
                _end = value;
                logiclCheck();
                base.RaisePropertyChanged(PropertyHelper.GetName(() => End));
            }
        }

        [Required(ErrorMessage = "登録者名は必須項目です。")]
        public string Subscriber
        {
            get { return _subscriber; }
            set
            {
                var propertyName = PropertyHelper.GetName(() => Subscriber);
                base.RemoveItemValidationError(propertyName);
                _subscriber = value;
                base.RaisePropertyChanged(propertyName);
            }
        }

        public string LogicalError
        {
            get { return _logicalError; }
            set
            {
                _logicalError = value;
                base.RaisePropertyChanged(PropertyHelper.GetName(() => LogicalError));
            }
        }

        private void addReservationCommandExecute()
        {
            base.ViewModelState.RemoveErrorByKey(PropertyHelper.GetName(() => LogicalError));
            base.ViewModelState.RemoveErrorByKey(PropertyHelper.GetName(() => Start));
            base.ViewModelState.RemoveErrorByKey(PropertyHelper.GetName(() => End));
            var isError = _service.ReserveRoom(RoomId, Start, End, Subscriber);
            switch (isError)
            {
                case IsReservError.StartTime:
                    base.ViewModelState.AddError(PropertyHelper.GetName(() => Start), "会議時間が競合しています。");
                    break;
                case IsReservError.EndTime:
                    base.ViewModelState.AddError(PropertyHelper.GetName(() => End), "会議時間が競合しています。");
                    break;
                case IsReservError.BothTime:
                    base.ViewModelState.AddError(PropertyHelper.GetName(() => Start), "会議時間が競合しています。");
                    base.ViewModelState.AddError(PropertyHelper.GetName(() => End), "会議時間が競合しています。");
                    break;
                case IsReservError.Repository:
                    base.ViewModelState.AddError(PropertyHelper.GetName(() => LogicalError),
                        "DB 登録でエラーが発生しました。");
                    break;
            }
            base.RaisePropertyChanged(PropertyHelper.GetName(() => Reservations));
            base.RaisePropertyChanged(PropertyHelper.GetName(() => Start));
            base.RaisePropertyChanged(PropertyHelper.GetName(() => End));
            base.RaisePropertyChanged(PropertyHelper.GetName(() => IsValid));
        }
        private bool addReservationCommandCanExecute
        {
            get { return base.ViewModelState.IsValid; }
        }
        private ICommand addReservationCommand;
        public ICommand AddReservationCommand
        {
            get
            {
                if (addReservationCommand == null)
                    addReservationCommand = new RelayCommand(
                        param => addReservationCommandExecute(),
                        param => addReservationCommandCanExecute
                    );
                return addReservationCommand;
            }
        }

        private void logiclCheck()
        {
            var errorList = new List<string>();
            if (!_service.IsReservValid(Start, End))
            {
                errorList.Add("会議開始日時は終了日時より前の日時を指定してください。");
            }
            if (!_service.IsReservValid(Start, End))
            {
                errorList.Add("会議終了日時は開始日時より後の日時を指定してください。。");
            }
            var propertyName = PropertyHelper.GetName(() => LogicalError);
            base.ViewModelState.RemoveErrorByKey(propertyName);
            if (errorList.Count > 0)
            {
                base.ViewModelState.AddError(propertyName,
                    string.Join(Environment.NewLine, errorList));
            }
        }

        ~ReserveWindowViewModel()
        {
            System.Diagnostics.Debug.WriteLine("ReserveWindowViewModel destructed!");
        }
    }
}

プロジェクトに「ReserveWindow」ウィンドウを作成します。

<Window x:Class="ValidationTestRoomReservation.ReserveWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:apps="clr-namespace:ValidationTestRoomReservation.UserControls"
        xmlns:viewModel="clr-namespace:ValidationTestRoomReservation.ViewModels"
        DataContext="{DynamicResource ReserveWindowViewModel}"
        Title="ReserveWindow" SizeToContent="WidthAndHeight">
    <Window.Resources>
        <viewModel:ReserveWindowViewModel x:Key="ReserveWindowViewModel" />
        <DataTemplate DataType="{x:Type ValidationError}">
            <TextBlock FontStyle="Italic" Foreground="Red" HorizontalAlignment="Right" Margin="0,1"
                           Text="{Binding Path=ErrorContent}" />
        </DataTemplate>
    </Window.Resources>
    <StackPanel>
        <TextBlock Text="{Binding Path=RoomName, StringFormat='[{0}]の予約状況'}"
                   FontSize="18" HorizontalAlignment="Center" />
        <DataGrid ItemsSource="{Binding Path=Reservations}" AutoGenerateColumns="False"
                  Margin="10 5" MaxHeight="150" IsReadOnly="True"
                  CanUserReorderColumns="False" CanUserSortColumns="False">
            <DataGrid.Resources>
                <Style TargetType="TextBlock">
                    <Setter Property="Padding" Value="10 2" />
                    <Setter Property="FontSize" Value="14" />
                </Style>
                <Style TargetType="TextBlock" x:Key="GridDataNumText">
                    <Setter Property="Padding" Value="10 2" />
                    <Setter Property="FontSize" Value="14" />
                    <Setter Property="TextAlignment" Value="Right" />
                </Style>
            </DataGrid.Resources>
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Path=Start, StringFormat='yyyy年MM月dd日 HH:mm'}"
                                    ElementStyle="{StaticResource GridDataNumText}">
                    <DataGridTextColumn.Header>
                        <TextBlock Text="開始日時" />
                    </DataGridTextColumn.Header>
                </DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Path=End, StringFormat='yyyy年MM月dd日 HH:mm'}"
                                    ElementStyle="{StaticResource GridDataNumText}">
                    <DataGridTextColumn.Header>
                        <TextBlock Text="終了日時" />
                    </DataGridTextColumn.Header>
                </DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Path=SubscriberName}"
                                    ElementStyle="{StaticResource GridDataNumText}">
                    <DataGridTextColumn.Header>
                        <TextBlock Text="予約者名" />
                    </DataGridTextColumn.Header>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
        <StackPanel Margin="50 20 20 20">
            <TextBlock Text="会議室の予約" FontSize="16" HorizontalAlignment="Center" />
            <TextBlock Text="{Binding Path=Error}" FontSize="12" Foreground="Red" HorizontalAlignment="Center" />
            <StackPanel Orientation="Horizontal">
                <Label Name="LabelStart" Content="開始時間" FontSize="12" HorizontalAlignment="Center" />
                <apps:DateTimePicker x:Name="StartDate"
                                     DateTimeValue="{Binding Path=Start, ValidatesOnDataErrors=True}" />
            </StackPanel>
            <ContentPresenter Content="{Binding ElementName=StartDate, Path=(Validation.Errors).CurrentItem}" />
            <StackPanel Orientation="Horizontal">
                <Label Content="終了時間" FontSize="12" HorizontalAlignment="Center" />
                <apps:DateTimePicker x:Name="EndDate"
                                     DateTimeValue="{Binding Path=End, ValidatesOnDataErrors=True}" />
            </StackPanel>
            <ContentPresenter Content="{Binding ElementName=EndDate, Path=(Validation.Errors).CurrentItem}" />
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="予約者:" FontSize="12" Width="{Binding ElementName=LabelStart, Path=ActualWidth}" />
                <TextBox Name="InputSubscriber" FontSize="12" MinWidth="100"
                         Text="{Binding Path=Subscriber, ValidatesOnDataErrors=True}" />
            </StackPanel>
            <ContentPresenter Content="{Binding ElementName=InputSubscriber, Path=(Validation.Errors).CurrentItem}" />
            <Button Content="登録" Command="{Binding Path=AddReservationCommand}" Margin="8" Padding="20 5" />
        </StackPanel>
    </StackPanel>
</Window>

ReserveWindow のコードビハインドに生成と破棄のデバッグメッセージ表示を追加します。

//
        public ReserveWindow()
        {
            System.Diagnostics.Debug.WriteLine("ReserveWindow created!");
            InitializeComponent();
        }

        ~ReserveWindow()
        {
            System.Diagnostics.Debug.WriteLine("ReserveWindow destructed!");
        }

最後に「MainWindow」です。

<Window x:Class="ValidationTestRoomReservation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:viewModel="clr-namespace:ValidationTestRoomReservation.ViewModels"
        DataContext="{DynamicResource MainWindowViewModel}"
        Title="MainWindow" SizeToContent="WidthAndHeight">
    <Window.Resources>
        <viewModel:MainWindowViewModel x:Key="MainWindowViewModel" />
        <Style TargetType="TextBlock" x:Key="GridDataText">
            <Setter Property="Padding" Value="10 2" />
            <Setter Property="FontSize" Value="14" />
        </Style>
        <Style TargetType="TextBlock" x:Key="GridDataNumText">
            <Setter Property="Padding" Value="10 2" />
            <Setter Property="FontSize" Value="14" />
            <Setter Property="TextAlignment" Value="Right" />
        </Style>
    </Window.Resources>
    <StackPanel>
        <TextBlock Text="会議室予約システム" FontSize="18" HorizontalAlignment="Center" />
        <DataGrid ItemsSource="{Binding Path=Rooms}" AutoGenerateColumns="False"
                  SelectedIndex="{Binding Path=SelectedRow, Mode=OneWayToSource}"
                  Margin="10 5" IsReadOnly="True" CanUserReorderColumns="False" CanUserSortColumns="False">
            <DataGrid.Resources>
                <Style TargetType="TextBlock">
                    <Setter Property="Padding" Value="10 2"/>
                    <Setter Property="FontSize" Value="14" />
                </Style>
            </DataGrid.Resources>
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Path=Name}" ElementStyle="{StaticResource GridDataText}">
                    <DataGridTextColumn.Header>
                        <TextBlock Text="会議室名" />
                    </DataGridTextColumn.Header>
                </DataGridTextColumn>
                <DataGridTextColumn Binding="{Binding Path=Capacity, StringFormat='#,0'}"
                                    ElementStyle="{StaticResource GridDataNumText}">
                    <DataGridTextColumn.Header>
                        <TextBlock Text="定員" />
                    </DataGridTextColumn.Header>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
        <Button Content="予約状況表示" FontSize="14" Margin="10,5" Command="{Binding Path=ShowRoomCommand}" />
        <Button Content="GC 実行" FontSize="14" Margin="10 5" Command="{Binding Path=GcCommand}" />
    </StackPanel>
</Window>

長くなりましたが、以上で完成です。


コメントを残す

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