前回
WinUI 3 の開発環境を整えました。
今回
基礎練です。お試し実装をしてみました。
事前準備
INotifyPropertyChanged 地獄 から逃れるために、ViewModelのベースを作ります。
.NET Community MVVM Toolkit パイセンお世話になります。
Csharp ViewModelCommon.cs
// 検証もしたいので、ObservableObjectじゃなくてObservableValidator
public partial class ViewModelCommon : ObservableValidator
{
// エラーメッセージをプロパティ毎に扱う用
public Dictionary<string, string> ErrorMessage { get; set; } = new Dictionary<string, string>();
public ViewModelCommon()
{
// キーがないと怒られるので、全部のプロパティを登録
foreach (var p in GetType().GetProperties().Select(r => r.Name))
{
ErrorMessage.Add(p, string.Empty);
}
// エラーの状態変更イベントを補足する
this.ErrorsChanged += ViewModelCommon_ErrorsChanged;
}
// エラーの状態が変わったら、ErrorMessageを作り直す
private void ViewModelCommon_ErrorsChanged(object? sender, DataErrorsChangedEventArgs e)
{
if (e.PropertyName != null)
{
// エラーメッセージが変更されることを通知
OnPropertyChanging(nameof(ErrorMessage));
ErrorMessage[e.PropertyName] = GetErrors(e.PropertyName)
.Select(r => r.ErrorMessage).DefaultIfEmpty(string.Empty)
.Aggregate((r, l) => $"{r}{Environment.NewLine}{l}")!;
// エラーメッセージが変更されたことを通知
OnPropertyChanged(nameof(ErrorMessage));
}
}
}
XMALから呼び出し可能なヘルパーメソッドを用意する。
Csharp ViewHelper.cs
public class ViewHelper
{
private ViewHelper() { }
// ViewModelの特定プロパティのエラーがあるか判定する
public static bool HasError(Dictionary<string, string> ErrorMessage, string propertyName)
{
return ErrorMessage.ContainsKey(propertyName) && !string.IsNullOrEmpty(ErrorMessage[propertyName]);
}
// boolを反転する
public static bool Invert(bool b)
{
return !b;
}
}
テーマを切り替えたい
OSの設定ではなく、アプリだけテーマを切り替えて確認したいです。
トグルスイッチでテーマを変更できるようにしてみたいと思います。
画面デザイン
xaml
xaml MainWindow.xaml
<Grid Background="{ThemeResource ControlFillColorDefaultBrush}" CornerRadius="{ThemeResource OverlayCornerRadius}" Margin="16 16 16 0" Padding="8 8 8 8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.1*" MinWidth="50" />
<ColumnDefinition Width="0.3*" />
<ColumnDefinition Width="0.5*" MinWidth="200"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<!-- アイコン -->
<FontIcon Glyph="" />
<!-- ラベル -->
<TextBlock Grid.Column="1" VerticalAlignment="Center" Text="ToggleSwitch & Change Theme" TextTrimming="CharacterEllipsis" />
<!--
トグルスイッチ
トグルスイッチがトグルったらテーマを変更する
トグルスイッチの状態をViewModelにBindingする
-->
<ToggleSwitch x:Name="toggleSwitch" Grid.Column="2" HorizontalAlignment="Right" IsOn="{x:Bind ViewModel.ToggleSwitchValue, Mode=TwoWay}" Toggled="toggleSwitch_Toggled" />
<!--
「ライトモード」を表示するInfoBar
bool値を反転するのヘルパーで出来るのマジカミ。
-->
<InfoBar Grid.Row="2" Grid.ColumnSpan="3"
IsOpen="{x:Bind local:ViewHelper.Invert(ViewModel.ToggleSwitchValue), Mode=OneWay}"
IsClosable="False"
Severity="Informational"
Message="ライトモード" Margin="8 4 0 4" CornerRadius="{ThemeResource ControlCornerRadius}" />
<!--
「ダークモード」を表示するInfoBar
-->
<InfoBar Grid.Row="2" Grid.ColumnSpan="3"
IsOpen="{x:Bind ViewModel.ToggleSwitchValue, Mode=OneWay}"
IsClosable="False"
Severity="Informational"
Message="ダークモード" Margin="8 4 0 4" CornerRadius="{ThemeResource ControlCornerRadius}" />
</Grid>
View
Csharp MainWindow.xmal.cs
public sealed partial class MainWindow : Window
{
// ViewModelを宣言しておくと、XAMLでも見える
public MainWindowViewModel ViewModel { set; get; }
public MainWindow()
{
InitializeComponent();
// ViewModelを初期化
ViewModel = new MainWindowViewModel();
// 現在のテーマを判定
ViewModel.ToggleSwitchValue = App.Current.RequestedTheme == ApplicationTheme.Dark;
}
// トグルスイッチがトグルったらテーマを変更する
private void toggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
var requestTheme = (sender as ToggleSwitch)!.IsOn ? ElementTheme.Dark : ElementTheme.Light;
FrameworkElement rootElement = (this.Content as FrameworkElement)!;
if (rootElement.ActualTheme != requestTheme)
{
rootElement.RequestedTheme = requestTheme;
}
}
}
現状、このWindowの中だけテーマが変わっている感。
アプリ全体のテーマを変える方法がわかりませんでしたので、募集中。
Application.Current.RequestedTheme = ApplicationTheme.Dark;
とすると、COMExceptionがでるんだが?
ViewModel
Csharp MainWindowViewModel.cs
// ViewModelの共通クラスを継承
public partial class MainWindowViewModel : ViewModelCommon
{
// トグルスイッチの状態
[ObservableProperty]
public partial bool ToggleSwitchValue { get; set; };
public MainWindowViewModel() : base()
{
}
}
入力内容を検証したい
入力された数値を、検証してみましょう。
画面デザイン
xaml
xaml MainWindow.xaml
<Grid Background="{ThemeResource ControlFillColorDefaultBrush}" CornerRadius="{ThemeResource OverlayCornerRadius}" Margin="16 16 16 0" Padding="8 8 8 8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.1*" MinWidth="50" />
<ColumnDefinition Width="0.3*" />
<ColumnDefinition Width="0.5*" MinWidth="300"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<!-- アイコン -->
<FontIcon Glyph="" />
<!-- ラベル -->
<TextBlock Grid.Column="1" VerticalAlignment="Center" Text="NumberBox & Validation" TextTrimming="CharacterEllipsis" />
<!--
数値入力はNumberBoxを使う
数値の形式、検証処理はC#側で行います。
-->
<NumberBox x:Name="numberBox" Grid.Column="2" HorizontalAlignment="Stretch"
Header="1~1,000の整数を入力してください。" Value="{x:Bind Path=ViewModel.NumberBoxValue, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
<!--
検証結果にエラーがあればInfoBarで表示する。
-->
<InfoBar Grid.Row="2" Grid.ColumnSpan="3"
IsOpen="{x:Bind local:ViewHelper.HasError(ViewModel.ErrorMessage, 'NumberBoxValue'), Mode=OneWay}"
IsClosable="False"
Severity="Error"
Message="{x:Bind ViewModel.ErrorMessage['NumberBoxValue'], Mode=OneWay}" Margin="8 4 0 4" CornerRadius="{ThemeResource ControlCornerRadius}" />
</Grid>
View
Csharp MainWindow.xmal.cs
public sealed partial class MainWindow : Window
{
// ViewModelを宣言
public MainWindowViewModel ViewModel { set; get; }
public MainWindow()
{
InitializeComponent();
// ViewModelを初期化
ViewModel = new MainWindowViewModel();
// 数値のフォーマットを指定する。今回は整数。カンマあり。小数部は切り下げ。
numberBox.NumberFormatter = new DecimalFormatter()
{
IntegerDigits = 1,
FractionDigits = 0,
NumberRounder = new IncrementNumberRounder() {
Increment= 1,
RoundingAlgorithm = RoundingAlgorithm.RoundDown
},
IsGrouped = true,
};
}
}
ViewModel
Csharp MainWindowViewModel.cs
// ViewModelの共通クラスを継承
public partial class MainWindowViewModel : ViewModelCommon
{
[ObservableProperty]
// 検証対象にする
[NotifyDataErrorInfo]
// 検証内容を設定する
[Range(1.0, 1000.0, ErrorMessage = "1~1000で入力してください。")]
public partial double NumberBoxValue { get; set; } = 1;
public MainWindowViewModel() : base()
{
}
}
日時を指定したい
入力された日付と時刻を、合わせて表示してみましょう。
画面デザイン
xaml
xaml MainWindow.xaml
<Grid Background="{ThemeResource ControlFillColorDefaultBrush}" CornerRadius="{ThemeResource OverlayCornerRadius}" Margin="16 16 16 0" Padding="8 8 8 8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.1*" MinWidth="50" />
<ColumnDefinition Width="0.3*" />
<ColumnDefinition Width="0.5*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<!-- アイコン -->
<FontIcon Glyph="" />
<!-- ラベル -->
<TextBlock Grid.Column="1" VerticalAlignment="Center" Text="DatePicker & TimePicker" TextTrimming="CharacterEllipsis" />
<!-- DatePickerとTimePicker -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
<DatePicker x:Name="datePicker" Date="{x:Bind ViewModel.DatePickerValue, Mode=TwoWay}" Header="日付を指定してください。" />
<TimePicker x:Name="timePicker" Time="{x:Bind ViewModel.TimePickerValue, Mode=TwoWay}" Header="時間を指定してください。" />
</StackPanel>
<!-- 年月日時分形式で表示 -->
<InfoBar Grid.Row="2" Grid.ColumnSpan="4"
IsOpen="True"
IsClosable="False"
Severity="Informational"
Message="{x:Bind ViewModel.DateTimeValue.ToString('yyyy/MM/dd HH:mm', x:Null), Mode=OneWay}" Margin="8 4 0 4" CornerRadius="{ThemeResource ControlCornerRadius}" />
</Grid>
View
Csharp MainWindow.xmal.cs
public sealed partial class MainWindow : Window
{
// ViewModelを宣言
public MainWindowViewModel ViewModel { set; get; }
public MainWindow()
{
InitializeComponent();
// ViewModelを初期化
ViewModel = new MainWindowViewModel();
// 日付、時刻を現在時刻で初期化
var now = DateTimeOffset.Now;
ViewModel.DatePickerValue = now.Date;
ViewModel.TimePickerValue = TimeSpan.FromHours(now.Hour).Add(TimeSpan.FromMinutes(now.Minute));
}
}
ViewModel
Csharp MainWindowViewModel.cs
public partial class MainWindowViewModel : ViewModelCommon
{
[ObservableProperty]
// 日付が変わったらDateTimeValueも変更されることを通知
[NotifyPropertyChangedFor(nameof(DateTimeValue))]
public partial DateTimeOffset DatePickerValue { get; set; } = DateTimeOffset.Now.Date;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DateTimeValue))]
public partial TimeSpan TimePickerValue { get; set; } = TimeSpan.Zero;
// 日付と時刻を連結
public DateTime DateTimeValue
{
get
{
return DatePickerValue.Date.Add(TimePickerValue);
}
}
public MainWindowViewModel() : base()
{
}
}
また、次回があればどうぞ。