web-dev-qa-db-ja.com

ItemsControlのWPFMVVMラジオボタン

私は以前に列挙型をラジオボタンにバインドしましたが、それがどのように機能するかは一般的に理解しています。この質問の代替実装を使用しました: RadioButtonsを列挙型にバインドする方法は?

列挙型の代わりに、ランタイムで列挙されたカスタムタイプのセットを生成し、それらをラジオボタンのセットとして表示したいと思います。 ListViewプロパティを使用してランタイム列挙セットに対して機能するビューを取得しました。ItemsSourceプロパティとSelectedItemプロパティにバインドしているため、ViewModelが接続されています。正しく。今、私はラジオボタンでListViewからItemsControlに切り替えようとしています。

これが私が得た限りです:

<Window.Resources>
    <vm:InstanceToBooleanConverter x:Key="InstanceToBooleanConverter" />
</Window.Resources>

<!-- ... -->

<ItemsControl ItemsSource="{Binding ItemSelections}">
    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="{x:Type vm:ISomeType}">
            <RadioButton Content="{Binding Name}"
                         IsChecked="{Binding Path=SelectedItem, Converter={StaticResource InstanceToBooleanConverter}, ConverterParameter={Binding}}"
                         Grid.Column="0" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

InstanceToBooleanConverterは、他の質問のEnumToBooleanConverterと同じ実装です。 Equalsメソッドを呼び出すだけのように見えるので、これは正しいようです。

public class InstanceToBooleanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value.Equals(parameter);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value.Equals(true) ? parameter : Binding.DoNothing;
    }
}

私が今得ている問題は、ランタイム値をConverterParameterとして送信する方法がわからないことです。 (上記のコードで)試してみると、次のエラーが発生します:

タイプ「Binding」の「ConverterParameter」プロパティに「Binding」を設定することはできません。 「バインディング」は、DependencyObjectのDependencyPropertyにのみ設定できます。

ItemインスタンスにバインドしてIValueConverterに渡す方法はありますか?

22

ItemsControlを使用して放棄し、代わりにListBoxを使用する方がはるかに簡単であることがわかりました。

それはもっと重いかもしれませんが、それは主にそれがあなたのために重い物を持ち上げているからです。 RadioButton.IsCheckedListBoxItem.IsSelectedの間で双方向バインディングを行うのは本当に簡単です。 ListBoxItemの適切なコントロールテンプレートを使用すると、すべての選択ビジュアルを簡単に取り除くことができます。

<ListBox ItemsSource="{Binding Properties}" SelectedItem="{Binding SelectedItem}">
    <ListBox.ItemContainerStyle>
        <!-- Style to get rid of the selection visual -->
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <ContentPresenter />
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
    <ListBox.ItemTemplate>
        <DataTemplate DataType="{x:Type local:SomeClass}">
            <RadioButton Content="{Binding Name}" GroupName="Properties">
                <!-- Binding IsChecked to IsSelected requires no support code -->
                <RadioButton.IsChecked>
                    <Binding Path="IsSelected"
                             RelativeSource="{RelativeSource AncestorType=ListBoxItem}"
                             Mode="TwoWay" />
                </RadioButton.IsChecked>
            </RadioButton>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
37

私の知る限り、MultiBindingを使用してこれを行う良い方法はありませんが、最初はそうなると思います。 ConverterParameterをバインドできないため、ConvertBack実装には必要な情報がありません。

私が行ったことは、列挙型をラジオボタンにバインドすることのみを目的として、別個のEnumModelクラスを作成したことです。 ItemsSourceプロパティでコンバーターを使用すると、EnumModelにバインドされます。 EnumModelは、バインディングを可能にするための単なるフォワーダーオブジェクトです。これは、列挙型の1つの可能な値と、ビューモデルへの参照を保持しているため、ビューモデルのプロパティをブール値との間で変換できます。

これはテストされていないが一般的なバージョンです:

<ItemsControl ItemsSource="{Binding Converter={StaticResource theConverter} ConverterParameter="SomeEnumProperty"}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <RadioButton IsChecked="{Binding IsChecked}">
                <TextBlock Text="{Binding Name}" />
            </RadioButton>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

コンバーター:

public class ToEnumModelsConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var viewmodel = value;
        var prop = viewmodel.GetType().GetProperty(parameter as string);

        List<EnumModel> enumModels = new List<EnumModel>();

        foreach(var enumValue in Enum.GetValues(prop.PropertyType))
        {
            var enumModel = new EnumModel(enumValue, viewmodel, prop);
            enumModels.Add(enumModel);
        }

        return enumModels;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

EnumModel:

public class EnumModel : INPC
{
    object enumValue;
    INotifyPropertyChanged viewmodel;
    PropertyInfo property;

    public EnumModel(object enumValue, object viewmodel, PropertyInfo property)
    {
        this.enumValue = enumValue;
        this.viewmodel = viewmodel as INotifyPropertyChanged;
        this.property = property;

        this.viewmodel.PropertyChanged += new PropertyChangedEventHandler(viewmodel_PropertyChanged);
    }

    void viewmodel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == property.Name)
        {
            OnPropertyChanged("IsChecked");
        }
    }

    public bool IsChecked
    {
        get
        {
            return property.GetValue(viewmodel, null).Equals(enumValue);
        }
        set
        {
            if (value)
            {
                property.SetValue(viewmodel, enumValue, null);
            }
        }
    }
}

私が知っているコードサンプルが機能することを確認します(ただし、まだかなり洗練されていません-WIP!)、 http://code.google.com/p/pdx/source/browse/trunk/PDX/PDX/ Toolkit/EnumControl.xaml.cs 。これは私のライブラリのコンテキスト内でのみ機能しますが、DescriptionAttributeに基づいてEnumModelの名前を設定する方法を示しています。これは便利な場合があります。

4
default.kramer

あなたはとても近いです。 1つのコンバーターに2つのバインディングが必要な場合は、MultiBindingIMultiValueConverterが必要です。構文はもう少し冗長ですが、それほど難しくはありません。

編集:

これがあなたが始めるための小さなコードです。

バインディング:

<RadioButton Content="{Binding Name}"
        Grid.Column="0">
    <RadioButton.IsChecked>
        <MultiBinding Converter="{StaticResource EqualsConverter}">
            <Binding Path="SelectedItem"/>
            <Binding Path="Name"/>
        </MultiBinding>
    </RadioButton.IsChecked>
</RadioButton>

およびコンバーター:

public class EqualsConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return values[0].Equals(values[1]);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

2回目の編集:

上記のアプローチは、質問にリンクされている手法を使用して双方向バインディングを実装するのに役立ちません。これは、元に戻すときに必要な情報が利用できないためです。

私が信じる正しい解決策は、まっすぐなMVVMです。ビューのニーズに一致するようにビューモデルをコーディングします。コードの量は非常に少なく、コンバーターや面白いバインディングやトリックは必要ありません。

これがXAMLです。

<Grid>
    <ItemsControl ItemsSource="{Binding}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <RadioButton
                    GroupName="Value"
                    Content="{Binding Description}"
                    IsChecked="{Binding IsChecked, Mode=TwoWay}"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>

ビューモデルをシミュレートするためのコードビハインド:

        DataContext = new CheckBoxValueCollection(new[] { "Foo", "Bar", "Baz" });

およびいくつかのビューモデルインフラストラクチャ:

    public class CheckBoxValue : INotifyPropertyChanged
    {
        private string description;
        private bool isChecked;

        public string Description
        {
            get { return description; }
            set { description = value; OnPropertyChanged("Description"); }
        }
        public bool IsChecked
        {
            get { return isChecked; }
            set { isChecked = value; OnPropertyChanged("IsChecked"); }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class CheckBoxValueCollection : ObservableCollection<CheckBoxValue>
    {
        public CheckBoxValueCollection(IEnumerable<string> values)
        {
            foreach (var value in values)
                this.Add(new CheckBoxValue { Description = value });
            this[0].IsChecked = true;
        }

        public string SelectedItem
        {
            get { return this.First(item => item.IsChecked).Description; }
        }
    }
2
Rick Sladkey

X:Sharedについて知ったので(あなたの 他の質問 に感謝します)、私は以前の答えを放棄し、結局MultiBindingが行く方法であると言います。

XAML:

<StackPanel>
    <TextBlock Text="{Binding SelectedChoice}" />

    <ItemsControl ItemsSource="{Binding Choices}">
        <ItemsControl.Resources>
            <local:MyConverter x:Key="myConverter" x:Shared="false" />
        </ItemsControl.Resources>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <RadioButton>
                    <RadioButton.IsChecked>
                        <MultiBinding Converter="{StaticResource myConverter}" >
                            <Binding Path="DataContext.SelectedChoice" RelativeSource="{RelativeSource AncestorType=UserControl}" />
                            <Binding Path="DataContext" RelativeSource="{RelativeSource Mode=Self}" />
                        </MultiBinding>
                    </RadioButton.IsChecked>
                    <TextBlock Text="{Binding}" />
                </RadioButton>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</StackPanel>

ビューモデル:

class Viewmodel : INPC
{
    public Viewmodel()
    {
        Choices = new List<string>() { "one", "two", "three" };
        SelectedChoice = Choices[0];
    }

    public List<string> Choices { get; set; }

    string selectedChoice;
    public string SelectedChoice
    {
        get { return selectedChoice; }
        set
        {
            if (selectedChoice != value)
            {
                selectedChoice = value;
                OnPropertyChanged("SelectedChoice");
            }
        }
    }
}

コンバーター:

public class MyConverter : IMultiValueConverter
{
    object selectedValue;
    object myValue;

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        selectedValue = values[0];
        myValue = values[1];

        return selectedValue == myValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        if ((bool)value)
        {
            return new object[] { myValue, Binding.DoNothing };
        }
        else
        {
            return new object[] { Binding.DoNothing, Binding.DoNothing };
        }

    }
}
1
default.kramer