概要
UWPやWPFで列挙型から1つの値を選ぶ目的でComboBoxやListBoxを使用することはよくあります。
その場合、ItemSourceに列挙型の値のリストを入れる必要があります。
以前の記事では列挙型を指定することで、その型の全値を提供するMarkUpExtensionをXAML側でItemSourceに使用する方法を紹介しました。
しかし、XAML側で型を指定するため、その内容を動的に変更することは出来ません。
また、多くの用途において、SelectedItemの型と同じなので、できればSelectedItemに自動で合わせてくれたほうがよいです。
そこで今回は添付プロパティを用いて、SelectedItemに合わせてItemSourceを自動生成します。
実行結果
2つListBoxが並んでおり、片方を選択するともう片方に伝わります。
さらに下のCheckBoxをクリックするとListBoxの候補値が切り替わります。
View
Viewは
(左)VMのItemSource
とSelectedItem
に同名のプロパティをバインドした通常のListBox
(右)今回紹介する添付プロパティをVMのSelectedItem
のみにバインドしたListBox
の2つがあります。
さらにその下のCheckBoxはIsChecked
がVMのIsDayOfWeek
にバインドしてあります。
<Window
x:Class="SelectorAttachedEnumProperty.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SelectorAttachedEnumProperty"
Width="325" Height="250">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<UniformGrid Grid.Row="0" Rows="1">
<!-- 普通に書いたの -->
<ListBox ItemsSource="{Binding ItemSource}" SelectedItem="{Binding SelectedItem}" />
<!-- 添付プロパティで指定 -->
<ListBox local:AutoEnumSource.SelectedEnumItem="{Binding SelectedItem}" />
</UniformGrid>
<!-- Checkを変更するとItemSourceとSelectedItemが切り替わる -->
<CheckBox
Grid.Row="1"
Content="IsDayOfWeek"
IsChecked="{Binding IsDayOfWeek}" />
</Grid>
</Window>
ViewModel
ViewModelでは
・ItemSource
通常のListBoxで使用するための列挙値の配列プロパティ
・SelectedItem
選択された列挙値プロパティ
・IsDayOfWeek
列挙型の切り替えプロパティ
が変更通知プロパティとして実装されています。
またIsDayOfWeek
が変更された場合、ItemSource
とSelectedItem
の中身がdayOfWeeks
⇔dateTimeKinds
全値配列間で変更されます。
今回の添付プロパティを使用した場合、ItemSource
とその元の2つの配列(dayOfWeeks
, dateTimeKinds
)は不要になります。
その場合の切り替え方法はSelectedItem
へのDayOfWeekとDateTimeKindの任意の値の入力です。
public class MainWindowViewModel : INotifyPropertyChanged
{
//INotifyPropertyChanged実装
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName]string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private List<object> dayOfWeeks = typeof(DayOfWeek).GetEnumValues().Cast<object>().ToList();
private List<object> dateTimeKinds = typeof(DateTimeKind).GetEnumValues().Cast<object>().ToList();
//添付プロパティでItemSourceは不要になる
private List<object> _ItemSource;
public List<object> ItemSource
{
get => _ItemSource;
set
{
if (_ItemSource == value)
return;
_ItemSource = value;
NotifyPropertyChanged();
}
}
private object _SelectedItem;
public object SelectedItem
{
get => _SelectedItem;
set
{
if (_SelectedItem == value)
return;
_SelectedItem = value;
NotifyPropertyChanged();
}
}
private bool _IsDayOfWeek = false;
public bool IsDayOfWeek
{
get => _IsDayOfWeek;
set
{
if (_IsDayOfWeek == value)
return;
_IsDayOfWeek = value;
ItemSource = value ? dayOfWeeks : dateTimeKinds;
SelectedItem = value ? (object)DayOfWeek.Monday : DateTimeKind.Unspecified;
NotifyPropertyChanged();
}
}
public MainWindowViewModel()
{
IsDayOfWeek = true;
}
}
添付プロパティ実装
添付プロパティの基本的な説明などは
WPF4.5入門 その45 「添付プロパティ」 - かずきのBlog@hatena
などを参考にして下さい。
この添付プロパティの仕事は値が入力された際に、
・ItemSource
にその値の列挙型の全値リストを設定する
・SelectedItem
とこの添付プロパティをバインドする
の2点です。
つまり、UIからの変更は
ViewのSelectorのSelectedItem→添付プロパティのSelectedEnumItem→VMのSelectedItem
の順で伝わります。
逆にVMからの変更は
VMのSelectedItem→添付プロパティ→ViewのSelectorのSelectedItem
の順です。
さらに変更された値の実行型が違うなら、添付プロパティの変更時にViewのItemSourceが変更されます。
public class AutoEnumSource
{
public static object GetSelectedEnumItem(Selector control) => (object)control.GetValue(SelectedEnumItemProperty);
public static void SetSelectedEnumItem(Selector control, object value) => control.SetValue(SelectedEnumItemProperty, value);
public static readonly DependencyProperty SelectedEnumItemProperty = DependencyProperty.RegisterAttached(
"SelectedEnumItem",
typeof(object), typeof(AutoEnumSource),
//デフォルトBindingモードをTwoWayにするために、FrameworkPropertyMetadataを使用
new FrameworkPropertyMetadata()
{
PropertyChangedCallback = OnSelectedEnumItemChanged,
BindsTwoWayByDefault = true
});
private static void OnSelectedEnumItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
//添付プロパティの変更値が有効でなかったら無効
Type newType = e.NewValue?.GetType();
if (newType == null || !newType.IsEnum)
{
return;
}
//添付されたコントロールがSelector以外では無効
var selector = d as Selector;
//変更値の型が変更前と同じなら何もしない
if (selector?.SelectedItem?.GetType() == newType)
{
return;
}
//添付されたSelectorのItemSourceに列挙型の全値を入力
selector.ItemsSource = Enum.GetValues(newType);
//SelectedItemに直接値を入力
selector.SelectedItem = e.NewValue;
//SelectorのSelctedItemにこの添付プロパティを双方向Bindingする
var binding = new Binding()
{
Path = new PropertyPath(AutoEnumSource.SelectedEnumItemProperty),
RelativeSource = RelativeSource.Self,
Mode = BindingMode.TwoWay
};
selector.SetBinding(Selector.SelectedItemProperty, binding);
}
}
環境
VisualStudio2017
.NET Framework 4.7
C#7.1