значение свойства переопределяется родительским, даже если задано явно

#c# #wpf #inheritance #properties #triggers

#c# #wpf #наследование #свойства #триггеры

Вопрос:

Когда у меня есть элемент управления A , содержащий элемент управления B , есть свойства Prop , которые наследуются. Это означает, B.Prop что автоматически примет значение, A.Prop если B.Prop оно явно не задано. Насколько я знаю, IsEnabled есть такое свойство.

Теперь у меня ситуация, когда я устанавливаю значение B.IsEnabled явно, и все равно оно перезаписывается значением A.IsEnabled . Почему это так, и как я могу это исправить?

В этой ситуации A это панель стека и B текстовое поле:

 <StackPanel>
    <StackPanel.Style>
        <Style TargetType="StackPanel">
            <Setter Property="IsEnabled" Value="True"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding InDisableMode}" Value="True">
                    <Setter Property="IsEnabled" Value="False"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </StackPanel.Style>
    <TextBox Text="some text">
        <TextBox.Style>
            <Style TargetType="TextBox">
                <Setter Property="IsEnabled" Value="False"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding InDisableMode}" Value="True">
                        <Setter Property="IsEnabled" Value="True"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </TextBox.Style>
    </TextBox>
</StackPanel>
  

Приведенный выше фрагмент XAML имеет DataContext, установленный для my ViewModel. ViewModel содержит свойство InDisableMode , которое является bool . Когда это так false , все происходит так, как ожидалось: StackPanel включена, а текстовое поле отключено.

Но когда InDisableMode это true так, и панель стека, и текстовое поле отключены, хотя оба триггера должны срабатывать!

Я знаю, что могу привязать IsEnabled данные InDisableMode к обоим элементам управления (непосредственно в текстовом поле, в стековой панели с помощью дополнительного конвертера). Я не пробовал, работает ли это, так как я все равно хочу сделать это с помощью триггеров.

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

Смысл отключения StackPanel заключается в том, чтобы легко отключить все его дочерние элементы (кроме текстового поля, которое я хочу включить вместо этого). Любые другие идеи, как решить эту задачу без изменения отношения родитель-потомок или создания новых элементов управления? На данный момент единственный способ, который я вижу, — отключить все дочерние элементы, кроме текстового поля, один за другим…

Ответ №1:

Если вы отключите элемент управления, его дочерние элементы будут отключены. Поскольку a StackPanel не является интерактивным, нет никаких причин для его отключения, кроме как для отключения его интерактивных дочерних элементов.

Если вы хотите включить элемент управления A, в то время как его родительский элемент B отключен, вы не можете этого сделать. B не может быть родительским, если вы хотите включить A, пока B отключен.

Для обходного пути вы можете поместить их оба в a Grid с TextBox определенным последним, чтобы наложить TextBox поверх StackPanel . Тогда оно будет находиться в области StackPanel ‘s, но не будет дочерним элементом StackPanel .

Ответ №2:

Это происходит потому UIElement.IsEnabled , что свойство использует принуждение к значению, наследуя значение от своего родителя. Это делается с помощью CoerceValueCallback . Принуждение к значению занимает первое место в списке приоритетов настройки свойств зависимостей.

Итак, чтобы переопределить это поведение, у нас есть два варианта. Во-первых, для использования AddOwner() для регистрации нашего типа в качестве нового владельца IsEnabled свойства. Во-вторых, для переопределения метаданных с помощью OverrideMetadata() . Этот второй метод будет работать, только если вы наследуете напрямую от UIElement .

Итак, допустим, мы хотим, чтобы наше Button поведение было другим, мы должны создать новое Button , подобное приведенному ниже :

 public class CButton : Button
{
    public static readonly DependencyProperty IsEnabled;

    static CButton()
    {
        IsEnabled = UIElement.IsEnabledProperty.AddOwner(typeof(CButton), 
                new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.None, 
                    UIElement.IsEnabledProperty.DefaultMetadata.PropertyChangedCallback,
                    new CoerceValueCallback(IsEnabledCoerceCallback)));
    }

    private static object IsEnabledCoerceCallback(DependencyObject d, object baseValue)
    {            
        return (bool) baseValue;
    }
}
  

Здесь мы возвращаем присвоенное значение в том виде, в каком оно есть IsEnabledCoerceCallback . Перед возвратом вы также можете ввести поведение: если пользователь не предоставляет никакого значения для IsEnabled , затем используйте унаследованное значение от родительского, иначе используйте CButton.IsEnabled назначенное пользователем значение.

Кстати, попробуйте установить null вместо new CoerceValueCallback(IsEnabledCoerceCallback) , посмотрите, что произойдет.

Комментарии:

1. «Это происходит потому, что UIElement. Свойство IsEnabled использует принуждение к значению, наследуя значение от своего родителя «. — но если дочерний элемент всегда наследует значение от своего родительского элемента, почему возможно включить родительский элемент, а дочерний элемент отключить?

2. @Kjara Наследование и перезапись — это два разных случая. Принудительное означает, что вы хотите перезаписать поведение по умолчанию. Label. FontSize = 20 изменит размер шрифта всех текстовых блоков внутри него, но если вы установите размер шрифта определенного, это будет соблюдено.