前々回と前回で書いた ValidationViewModelBase と DateTimePicker を利用して、会議室予約システムを作ってみます(ただし、あくまで利用例なので、予定の更新、削除、予定一覧の絞込みやページング表示、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>
長くなりましたが、以上で完成です。