[XAML] WPF で Resources に Binding を定義して、共通化して使用する方法

2024-01-27 (土)

通常は Style の Setter を使用することで、共通のバインドを定義することができますが、別々の Style を適用したい場合などは別の方法が必要です。
ResourceDictionary に x:Key を指定して Binding を定義しても、StaticResource から参照することができません。
Markup を実装することで、共通の Binding を1回だけ定義して、共有して使用する方法です。

環境

  • .NET 8.0.100
  • C# 12.0
  • Visual Studio 2022 Version 17.8.5
  • Windows 11 Pro 22H2 22621.3007

前提

本記事の実装方法は、XAML の IntelliSense が完全に機能し、タイプセーフなコードになる実装方法にしています。
コードの記述量や性能よりも、タイプセーフを重視しています。

タイプセーフな XAML については、以下の記事を参照してください。

[XAML] WPF で IntelliSense による入力補完やリファクタ機能を可能にする記述方法

バインドするプロパティをタイプセーフに記述します。WPF で ReSharper (Rider) を使用する想定です。
バインドのプロパティ名に誤りがあれば、IDE 上で警告されます。
定義へ移動(Go to Declaration)、すべての参照を検索(Find Usages)、リファクタによる名前変更(Rename)などの機能もフルに利用可能です。

XAML XAML WPF 更新: 2024-01-22 (月)

結果

使用箇所のコードを抜粋して示します。詳しいコードは後述の使用例を参照してください。

まずバインドするために、独自のマークアップ拡張を実装します。

using System;
using System.Diagnostics;
using System.Windows.Data;
using System.Windows.Markup;

namespace WpfApp.Samples;

public sealed class BindingResource
{
    public Binding Binding { get; set; } = null!;
}

[MarkupExtensionReturnType(typeof(object))]
public sealed class BindingResourceExtension : MarkupExtension
{
    public BindingResource Resource { get; set; } = null!;

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return Resource.Binding.ProvideValue(serviceProvider);
    }
}

BindingResource を使用して XAML の Resoures に共通の Binding を定義します。
これを BindingResourceExtension マークアップで参照します。

<Window.Resources>
    <!-- 1. ここで共通の Binding を定義します -->
    <samples:BindingResource x:Key="Key.Sample" Binding="{Binding IsSampleBool, Mode=OneWay}" />
</Window.Resources>

<!-- 2. ここで共通の Binding を参照します -->
<TextBox Text="1" IsEnabled="{samples:BindingResource Resource={StaticResource Key.Sample}}" />

DataGridColumn で使用する場合は、BindingProxy を使用します。
💡 Proxy を参照する際は、Mode=OneTime ではなく Mode=OneWay でバインドする必要があります。

<Window.Resources>
    <!-- 1. ViewModel をバインドする Proxy を定義します -->
    <samples:SampleViewModelBindingProxy x:Key="Proxy.ViewModel" Value="{Binding}" />

    <!-- 2. DataGridColumn で使用するため Source に Proxy 参照します (OneTime ではなく OneWay にします) -->
    <markup:BindingResource x:Key="Key.Sample" Binding="{Binding Source={StaticResource Proxy.ViewModel}, Path=Value.IsSampleBool, Mode=OneWay}" />
</Window.Resources>

<!-- 3. ここで共通の Binding を参照します -->
<DataGridTextColumn Header="1" Width="100" IsReadOnly="{markup:BindingResource Resource={StaticResource Key.Sample}}" />

使用例1. 通常のコントロールにバインドする例

Window と ViewModel クラス例です。

using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WpfApp.Samples
{
    public sealed partial class BindingResourceWindow
    {
        public BindingResourceWindow()
        {
            InitializeComponent();
        }
    }

    public sealed class BindingResourceViewModel : INotifyPropertyChanged
    {
        // この bool がバインド用のプロパティです
        public bool IsSampleBool { get => _isSampleBool; set => SetProperty(ref _isSampleBool, value); }
        private bool _isSampleBool=true;

        public event PropertyChangedEventHandler? PropertyChanged;

        private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
            field = value;
            OnPropertyChanged(propertyName);
            return true;
        }
    }
}

XAML 側の使用例です。

<Window x:Class="WpfApp.Samples.BindingResourceWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        xmlns:samples="clr-namespace:WpfApp.Samples"
        Title="BindingResourceWindow"
        Width="800"
        Height="450"
        d:DataContext="{d:DesignInstance {x:Type samples:BindingResourceViewModel}}">
    <Window.DataContext>
        <samples:BindingResourceViewModel />
    </Window.DataContext>

    <Window.Resources>
        <!-- 1. ここで共通の Binding を定義します -->
        <samples:BindingResource x:Key="Key.Sample" Binding="{Binding IsSampleBool, Mode=OneWay}" />
    </Window.Resources>

    <StackPanel>
        <CheckBox IsChecked="{Binding IsSampleBool, Mode=TwoWay}" />

        <!-- 2. ここで共通の Binding を参照します -->
        <TextBox Text="1" IsEnabled="{samples:BindingResource Resource={StaticResource Key.Sample}}" />
        <TextBox Text="2" IsEnabled="{samples:BindingResource Resource={StaticResource Key.Sample}}" />
        <TextBox Text="3" IsEnabled="{samples:BindingResource Resource={StaticResource Key.Sample}}" />
    </StackPanel>
</Window>

通常は Style の Setter を使用することで、共通のバインドを定義することができます。
しかし、3 個の TextBox に別々の Style を適用したい場合は、この方法が有効と思います。

使用例2. DataGridColumn にバインドする例

使用例1 のクラスを使用します。追加で Proxy クラスを定義します。
BindingProxy については、こちら を参照してください。

public sealed class SampleViewModelBindingProxy : BindingProxy<BindingResourceViewModel>;

XAML 側の使用例です。

<Window x:Class="WpfApp.Samples.BindingResourceWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        xmlns:samples="clr-namespace:WpfApp.Samples"
        Title="BindingResourceWindow"
        Width="800"
        Height="450"
        d:DataContext="{d:DesignInstance {x:Type samples:BindingResourceViewModel}}">
    <Window.DataContext>
        <samples:BindingResourceViewModel />
    </Window.DataContext>

    <Window.Resources>
        <!-- 1. ViewModel をバインドする Proxy を定義します -->
        <samples:SampleViewModelBindingProxy x:Key="Proxy.ViewModel" Value="{Binding}" />

        <!-- 2. DataGridColumn で使用するため Source に Proxy 参照します (OneTime ではなく OneWay にします) -->
        <markup:BindingResource x:Key="Key.Sample" Binding="{Binding Source={StaticResource Proxy.ViewModel}, Path=Value.IsSampleBool, Mode=OneWay}" />
    </Window.Resources>

    <DataGrid>
        <DataGrid.Columns>
            <!-- 3. ここで共通の Binding を参照します -->
            <DataGridTextColumn Header="1" Width="100" IsReadOnly="{markup:BindingResource Resource={StaticResource Key.Sample}}" />
            <DataGridTextColumn Header="2" Width="100" IsReadOnly="{markup:BindingResource Resource={StaticResource Key.Sample}}" />
            <DataGridTextColumn Header="3" Width="100" IsReadOnly="{markup:BindingResource Resource={StaticResource Key.Sample}}" />

            <!-- 本来ここで Binding を個別に定義する場合は、OneTime でも動作します -->
            <DataGridTextColumn Header="4" Width="100" IsReadOnly="{Binding Source={StaticResource Proxy.ViewModel}, Path=Value.IsSampleBool, Mode=OneTime}" />
        </DataGrid.Columns>
    </DataGrid>
</Window>

感謝

2024-01-27 (土)