LoginSignup
2
3

More than 5 years have passed since last update.

WPFでread-onlyなプロパティと双方向バインドする

Posted at

タイトル通りなのですが、日本語のページが見つからなかったのでメモ。
以下、ビヘイビアを使ってDataGridのSelectedItemsとバインドする例になります。

ビヘイビアの作成

behaviors/DataGridSelectedItemsBlendBehavior.cs
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace WpfApp3.Behaviors
{
    class DataGridSelectedItemsBlendBehavior : Behavior<DataGrid>
    {
        public static readonly DependencyProperty SelectedItemsProperty =
            DependencyProperty.Register(
                "SelectedItems",
                typeof(IList<object>),
                typeof(DataGridSelectedItemsBlendBehavior),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemsChanged)
            );

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SelectionChanged += OnSelectionChanged;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            if (AssociatedObject != null) AssociatedObject.SelectionChanged -= OnSelectionChanged;
        }

        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (SelectedItems == null) return;

            if (e.AddedItems != null)
            {
                foreach (var item in e.AddedItems) SelectedItems.Add(item);
            }

            if (e.RemovedItems != null)
            {
                foreach (var item in e.RemovedItems) SelectedItems.Remove(item);
            }
        }

        private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is DataGridSelectedItemsBlendBehavior behavior)
            {
                var dataGrid = behavior.AssociatedObject;

                if (behavior.SelectedItems != null && dataGrid?.SelectedItems != null)
                {
                    dataGrid.SelectionChanged -= behavior.OnSelectionChanged;

                    dataGrid.SelectedItems.Clear();
                    foreach (var item in behavior.SelectedItems)
                    {
                        dataGrid.SelectedItems.Add(item);
                    }

                    dataGrid.SelectionChanged += behavior.OnSelectionChanged;
                }
            }
        }

        public IList<object> SelectedItems
        {
            get { return (IList<object>)GetValue(SelectedItemsProperty); }
            set { SetValue(SelectedItemsProperty, value); }
        }
    }
}

依存関係プロパティの規約に沿って、static readonlyなDependencyPropertyのインスタンスを~Propertyという名前で作成します。
FrameworkPropertyMetaDataコンストラクタの第3引数にコールバックメソッドを指定するのを忘れないでください。
私はこれを忘れてソースからターゲットへの反映ができずハマりました。
OnSelectedItemsChangedでイベントハンドラを一度解除しているのは、ClearやAddごとにSelectionChangedが発生してしまうからです。
WpfApp3?そこに気づくとは…やはり天才か(適当)

View側の設定

Views/MainWindow.xaml
<Window x:Class="WpfApp3.Views.MainWindow"
        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"
        xmlns:local="clr-namespace:WpfApp3"
        xmlns:behaviors="clr-namespace:WpfApp3.Behaviors"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="25" />
            <RowDefinition Height="25" />
        </Grid.RowDefinitions>

        <DataGrid Grid.Row="0" ItemsSource="{Binding Members, Mode=OneTime}">
            <i:Interaction.Behaviors>
                <behaviors:DataGridSelectedItemsBlendBehavior SelectedItems="{Binding SelectedMembers}" />
            </i:Interaction.Behaviors>

            <i:Interaction.Triggers>
                <i:EventTrigger EventName="SelectedCellsChanged">
                    <i:InvokeCommandAction Command="{Binding SelectedCellsChangedCommand}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </DataGrid>

        <TextBox Grid.Row="1" Text="{Binding SelectedMembersNames, Mode=OneWay}" />

        <Button Grid.Row="2" Content="(・8・)" Command="{Binding ButtonClickCommand}" />
    </Grid>
</Window>

ViewでDataGridにビヘイビアの指定とSelectedItemsのバインド先を指定します。
TextBoxに選択中のアイテムを表示して、Buttonクリックでプログラム側から選択中のアイテムを変更する想定です。

ViewModelの作成

ViewModels/MainWindowViewModel.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;

using Prism.Commands;
using Prism.Mvvm;

namespace WpfApp3.ViewModels
{
    using Models;

    class MainWindowViewModel : BindableBase
    {
        public List<Person> Members { get; }

        private ObservableCollection<object> selectedMembers = new ObservableCollection<object>();
        public ObservableCollection<object> SelectedMembers
        {
            get { return selectedMembers; }
            set { SetProperty(ref selectedMembers, value); }
        }

        public string SelectedMembersNames => string.Join(", ", SelectedMembers.Select(p => ((Person)p).Name));

        public ICommand ButtonClickCommand { get; }
        public ICommand SelectedCellsChangedCommand { get; }

        public MainWindowViewModel()
        {
            Members = new List<Person>
            {
                new Person("Honoka Kousaka", 16),
                new Person("Eli Ayase", 17),
                new Person("Kotori Minami", 16),
                new Person("Umi Sonoda", 16),
                new Person("Rin Hoshizora", 15),
                new Person("Maki Nishikino", 15),
                new Person("Nozomi Tojo", 17),
                new Person("Hanayo Koizumi", 15),
                new Person("Nico Yazawa", 17),
            };

            ButtonClickCommand = new DelegateCommand(
                () => SelectedMembers = new ObservableCollection<object> { Members[2] });

            SelectedCellsChangedCommand = new DelegateCommand(
                () => RaisePropertyChanged(nameof(SelectedMembersNames)));
        }
    }
}

ViewModelです。PersonクラスはModels名前空間で適当に定義しています。
ボタンを押したり選択行を適当に変えたりしてTextBoxに正しく表示されることを確認してみてください。

ボタンをクリックした時
ボタンをクリックした時.png

行をいくつか選択した時
行をいくつか選択した時.png

余談

片方向でいいから直接バインドしたいと思っても、setterがないとOneWayToSourceですらバインドできないというWPFの仕様ェ…
あと、ModelにIsSelectedみたいなプロパティを準備できるならそれもありだと思います。
(・8・)

参考

WPF/MVVM: Binding to Read-Only Properties Using Behaviors - TechNet Articles
WPFのDataGridで選択された複数のアイテムをバインドするためのビヘイビア ※ターゲットからソースのバインド

2
3
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
2
3