WPF + MVVM — как привязать свойство к контексту данных родительского представления

Работая с шаблоном MVVM, у меня есть пара классов моделей представлений, представляющих двухуровневую иерархию данных, каждый из которых имеет соответствующий UserControl, представляющий его представление. Оба класса модели представления реализуют INotifyPropertyChanged, а модель представления корневого уровня предоставляет свойство, относящееся как к ее собственному представлению, так и к дочернему представлению.

Представление корневого уровня получает модель представления корневого уровня в качестве своего контекста данных и явно присваивает контекст данных содержащемуся в нем представлению. Однако также необходимо привязать одно из свойств дочернего представления к вышеупомянутому общему свойству. Вот как я пытался добиться этого, но это не работает:

<UserControl x:Name="rootView">
    <StackPanel>

        <!-- other controls here -->

        <my:ChildView
            DataContext="{Binding Path=SelectedChild}"
            EditingMode="{Binding ElementName=rootView, Path=DataContext.EditingMode />
    </StackPanel>
</UserControl>

Хотя ошибок привязки во время выполнения нет и дочернее представление правильно привязывается к соответствующему экземпляру модели дочернего представления, его свойство EditingMode никогда не устанавливается. Я провел тесты, чтобы убедиться, что соответствующее свойство модели представления изменяется и что оно уведомляет об этом изменении через INotifyPropertyChanged, но привязка не может его обнаружить.

Есть ли лучший способ объявить эту привязку, или я сделал более простую архитектурную ошибку?

Большое спасибо за ваш совет,

Тим

Обновление: В соответствии с запросом я публикую некоторый код, чтобы показать очень упрощенную версию моих представлений и моделей представлений, а также результаты проведенного мной эксперимента, которые могут дать некоторые дополнительные подсказки.

// The relevant parts of the ParentViewModel class
public class ParentViewModel : INotifyPropertyChanged
{
    // Although not shown, the following properties
    // correctly participate in INotifyPropertyChanged

    public ChildViewModel SelectedChild { get; private set; }

    public ContentEditingMode EditingMode { get; private set; }
}

// The relevant parts of the ChildViewModel class
public class ChildViewModel : INotifyPropertyChanged
{
    // No properties of ChildViewModel affect this issue.
}

// The relevant parts of the ParentView class
public partial class ParentView : UserControl
{
    // No properties of ParentView affect this issue.
}

// The relevant members of the ChildView class
public partial class ChildView : UserControl
{
    public static readonly DependencyProperty EditingModeProperty =
        DependencyProperty.Register(
            "EditingMode",
            typeof(ContentEditingMode),
            typeof(PostView)
        );

    public ContentEditingMode EditingMode
    {
        get { return (ContentEditingMode)GetValue(EditingModeProperty); }
        set { SetValue(EditingModeProperty, value); }
    }
}

// The enumeration used for the EditingMode property
public enum ContentEditingMode
{
    Html,
    WYSYWIG
}

Мое намерение состоит в том, что DataContext экземпляра родительского представления будет назначен экземпляр ParentViewModel, а он, в свою очередь, присвоит значение своего свойства SelectedChild DataContext вложенного ChildView. Все это вроде бы работает правильно, но возникает проблема, потому что привязка между ParentViewModel.EditingMode и ChildView.EditingMode не работает.

В попытке проверить, есть ли проблема с моим выражением привязки, я ввел TextBlock рядом с ChildView и привязал его аналогично свойству ParentViewModel.EditingMode:

<UserControl x:Name="rootView">
    <StackPanel>

        <!-- other controls here -->

        <TextBlock Text="{Binding ElementName=rootView, Path=DataContext.EditingMode}" />

        <my:ChildView
            DataContext="{Binding Path=SelectedChild}"
            EditingMode="{Binding ElementName=rootView, Path=DataContext.EditingMode />
    </StackPanel>
</UserControl>

В этом тесте TextBlock корректно обновляется каждый раз, когда изменяется исходное свойство. Однако, если я установлю точку останова на установщике ChildView.EditingMode, она никогда не сработает.

Я сбит с толку!


person Tim Coulter    schedule 03.07.2010    source источник
comment
Установка точки останова в установщике свойства зависимостей не сработает. Весь смысл свойств зависимостей в том, что они устанавливаются системой свойств зависимостей. Вот почему вы не можете привязываться к обычным свойствам CLR; привязки устанавливают целевые свойства, вызывая SetValue. Если вы хотите отслеживать настройку свойств зависимостей, вам необходимо реализовать функцию обратного вызова и использовать перегрузку Register, которая позволяет указать ее.   -  person Robert Rossney    schedule 03.07.2010
comment
Спасибо за это разъяснение - это, вероятно, сэкономит мне бесчисленные часы разочарования.   -  person Tim Coulter    schedule 03.07.2010
comment
Если бы я знал это, это спасло бы меня от бесчисленных часов разочарования, я могу вам многое сказать.   -  person Robert Rossney    schedule 04.07.2010


Ответы (2)


Самый простой способ исправить это — в вашей модели представления. Реализуйте свойство EditingMode в дочерней модели представления и привяжите его. Таким образом, вам не нужно делать какие-либо предположения о том, каким может быть правильный способ установления привязки; Кроме того, это то, что вы можете протестировать вне пользовательского интерфейса.

Изменить

На самом деле правильное решение не так просто, но стоит знать, как это сделать.

Вы хотите, чтобы EditingMode в дочернем элементе управления эффективно наследовало свое значение от родительского элемента управления. Похоже ли это на то, что делает что-то еще в WPF? Как и почти каждый элемент фреймворка, который реализует свойства зависимостей?

Реализуйте EditingMode как свойство зависимостей как в родительском, так и в дочернем UserControl и используйте наследование значений свойств, как описано здесь. Это полностью убирает поведение наследования из модели представления и помещает его туда, где ему и место.

person Robert Rossney    schedule 03.07.2010
comment
Спасибо за ваше предложение Роберт. На самом деле, Беррил сделал подобное предложение почти в то же время! Пожалуйста, смотрите мой ответный комментарий. Оправданы ли мои опасения по поводу производительности? - person Tim Coulter; 03.07.2010
comment
Спасибо, Роберт. Ваше редактирование — именно то нестандартное мышление, которое мне нужно! Я реализую это и опубликую снова, если столкнусь с какими-либо препятствиями, но тем временем я принимаю ваш ответ. Спасибо еще раз. - person Tim Coulter; 03.07.2010

Посмотрите, можете ли вы просто использовать полный путь, чтобы получить режим редактирования выбранного дочернего элемента:

<my:childView
        DataContext="{Binding SelectedChild}"
        EditingMode="{Binding SelectedChild.EditingMode />
person Berryl    schedule 03.07.2010
comment
Спасибо, Беррил, но это не работает, так как дочерняя модель представления не имеет свойства EditingMode. Это причина сообщения - как связать дочернее представление со свойством в модели родительского представления. - person Tim Coulter; 03.07.2010
comment
Итак, SelectedChild является свойством ParentVm, и EditingMode тоже? Это слишком просто, потому что тогда вы просто привязываетесь напрямую к EditingMode. Откуда вы получаете EditingMode, если не из RootLevelParentVm? Возможно, вам придется опубликовать код вашего UserControl. - person Berryl; 03.07.2010
comment
Я не могу напрямую привязаться к свойству EditingMode из дочернего представления, потому что DataContext, назначенный дочернему представлению, является экземпляром модели дочернего представления, которая не предоставляет это свойство. Как вы предлагаете, я опубликую упрощенную версию своего кода. - person Tim Coulter; 03.07.2010
comment
Хорошо, одна вещь, которую вы можете сделать, это сохранить ссылку на родителя и установить ее в конструкторе дочернего элемента. Я предполагаю, что Родитель создает ребенка; если это так, он просто передает себя этому дочернему конструктору. Теперь эта сложность находится в ваших моделях просмотра, а не в представлении. Просто добавьте любые родительские свойства, которые вам нужны, к дочернему элементу. Например: EditMode {get{return _parent.EditMode}}. - person Berryl; 03.07.2010
comment
Хм - я не уверен в этом. Хотя я согласен с тем, что это решит проблему, дочернее свойство реплики должно будет отслеживать INotifyPropertyChanged, а это означает, что потенциально сотни событий будут возникать в дочерних экземплярах каждый раз, когда изменяется родительское свойство. Не вызовет ли это проблемы с производительностью? - person Tim Coulter; 03.07.2010
comment
Да, вам нужно активировать измененное свойство, но оно будет только в EditMode SelectedChild (которое также активирует измененное свойство) в любой момент времени. Ничего страшного и не хит производительности. - person Berryl; 03.07.2010