ListBoxやComboBoxでItemSorceを自動生成する添付プロパティ

#概要
UWPやWPFで列挙型から1つの値を選ぶ目的でComboBoxやListBoxを使用することはよくあります。
その場合、ItemSourceに列挙型の値のリストを入れる必要があります。

[以前の記事](https://qiita.com/soi/items/6f36cb095af81500e8cb)では列挙型を指定することで、その型の全値を提供するMarkUpExtensionをXAML側でItemSourceに使用する方法を紹介しました。

しかし、XAML側で型を指定するため、その内容を動的に変更することは出来ません。
また、多くの用途において、SelectedItemの型と同じなので、できればSelectedItemに自動で合わせてくれたほうがよいです。
そこで今回は添付プロパティを用いて、SelectedItemに合わせてItemSourceを自動生成します。

#実行結果
![スクリーンショット 2017-11-14 22.42.42.png](https://qiita-image-store.s3.amazonaws.com/0/120077/368d32c2-c65a-749f-ba53-9106f5318685.png)
2つListBoxが並んでおり、片方を選択するともう片方に伝わります。
![スクリーンショット 2017-11-14 22.42.47.png](https://qiita-image-store.s3.amazonaws.com/0/120077/f82f2059-6273-1e14-15c8-e2313da4a24a.png)

さらに下のCheckBoxをクリックするとListBoxの候補値が切り替わります。
![スクリーンショット 2017-11-14 22.43.22.png](https://qiita-image-store.s3.amazonaws.com/0/120077/884fda3f-adc0-a30d-2db1-e7f211e185e4.png)

#View
Viewは
（左）VMの`ItemSource`と`SelectedItem`に同名のプロパティをバインドした通常のListBox
（右）今回紹介する添付プロパティをVMの`SelectedItem`のみにバインドしたListBox
の２つがあります。
さらにその下のCheckBoxは`IsChecked`がVMの`IsDayOfWeek`にバインドしてあります。

```xml:MainWindow.xaml
<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`とその元の２つの配列（`dayOfWeeks`, `dateTimeKinds`)は不要になります。
その場合の切り替え方法は`SelectedItem`へのDayOfWeekとDateTimeKindの任意の値の入力です。

```csharp:MainWindowViewModel
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](http://blog.okazuki.jp/entry/2014/08/20/081810)
などを参考にして下さい。

この添付プロパティの仕事は値が入力された際に、
・`ItemSource`にその値の列挙型の全値リストを設定する
・`SelectedItem`とこの添付プロパティをバインドする
の２点です。

つまり、UIからの変更は
ViewのSelectorのSelectedItem→添付プロパティのSelectedEnumItem→VMのSelectedItem
の順で伝わります。
逆にVMからの変更は
VMのSelectedItem→添付プロパティ→ViewのSelectorのSelectedItem
の順です。

さらに変更された値の実行型が違うなら、添付プロパティの変更時にViewのItemSourceが変更されます。

```csharp:AutoEnumSource.cs
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