WPF MVVM Радиокнопки на ItemsControl - PullRequest
22 голосов
/ 05 мая 2011

Ранее я связывал перечисления с переключателями, и я в целом понимаю, как это работает. Я использовал альтернативную реализацию из этого вопроса: Как связать 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. Когда я пытаюсь (с кодом выше), я получаю эту ошибку:

«Связывание» не может быть установлено в свойстве «ConverterParameter» типа «Связывание». «Привязка» может быть установлена ​​только в свойстве DependencyProperty объекта DependencyObject.

Есть ли способ привязки к экземпляру предмета и передачи его в IValueConverter?

Ответы [ 4 ]

36 голосов
/ 06 мая 2011

Оказывается, намного проще отказаться от использования ItemsControl и вместо этого перейти с ListBox.

Он может быть более тяжелым, но в основном потому, что он делает тяжелую работу за вас. Сделать двустороннюю привязку между RadioButton.IsChecked и ListBoxItem.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>
4 голосов
/ 05 мая 2011

Насколько я знаю, нет хорошего способа сделать это с MultiBinding, хотя вы изначально думаете, что будет. Поскольку вы не можете связать ConverterParameter, ваша реализация ConvertBack не имеет необходимой информации.

Я создал отдельный класс EnumModel исключительно для привязки перечисления к переключателям. Используйте конвертер для свойства ItemsSource, и тогда вы привязываетесь к EnumModel. EnumModel - это просто объект пересылки, который делает возможным связывание. Он содержит одно возможное значение перечисления и ссылку на модель представления, поэтому он может преобразовать свойство модели представления в логическое значение и обратно.

Вот непроверенная, но общая версия:

<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();
    }
}

Модель Enum:

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. Это работает только в контексте моей библиотеки, но демонстрирует установку имени EnumModel на основе DescriptionAttribute, который может быть полезен для вас.

2 голосов
/ 05 мая 2011

Вы так близко.Когда вам нужны две привязки для одного конвертера, вам нужны MultiBinding и IMultiValueConverter!Синтаксис немного более подробный, но не более сложный.

Редактировать:

Вот небольшой код для начала работы.

Привязка:

<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();
    }
}

Второе редактирование:

Приведенный выше подход бесполезен для реализации двустороннего связывания с использованием метода, связанного в вопросе, поскольку необходимая информация недоступна при обратном преобразовании..

Правильное решение, которое я считаю, это простой 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; }
        }
    }
1 голос
/ 06 мая 2011

Теперь, когда я знаю о 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 };
        }

    }
}
...