1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

初めての WinUI 3 (2) ViewとViewModel

Last updated at Posted at 2025-05-31

前回

WinUI 3 の開発環境を整えました。

今回

基礎練です。お試し実装をしてみました。

image.png

image.png

事前準備

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の設定ではなく、アプリだけテーマを切り替えて確認したいです。
トグルスイッチでテーマを変更できるようにしてみたいと思います。

画面デザイン

ライトモード
image.png
ダークモード
image.png

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="&#xF19E;" />
    <!-- ラベル -->
    <TextBlock Grid.Column="1" VerticalAlignment="Center" Text="ToggleSwitch &amp; 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()
        {
        }
    }

入力内容を検証したい

入力された数値を、検証してみましょう。

画面デザイン

初期状態
image.png
範囲外の数値を入力
image.png

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="&#xE961;" />
                <!-- ラベル -->
                <TextBlock Grid.Column="1" VerticalAlignment="Center" Text="NumberBox &amp; 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()
        {
        }
    }

日時を指定したい

入力された日付と時刻を、合わせて表示してみましょう。

画面デザイン

image.png

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="&#xEC92;" />
                <!-- ラベル -->
                <TextBlock Grid.Column="1" VerticalAlignment="Center" Text="DatePicker &amp; 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()
        {
        }
    }

また、次回があればどうぞ。

1
1
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?