Привязка имени элемента не работает в пользовательском элементе управления в Silverlight 4

#silverlight #xaml #silverlight-4.0 #binding

#silverlight #xaml #silverlight-4.0 #привязка

Вопрос:

У нас есть экран и его модель представления:

 public class ScreenViewModel : BaseViewModel
{
    [NotifyPropertyChanged]
    public List<Node> Nodes { get; set; }

    public ICommand NodeClickedCommand { get; set; }

    public ScreenViewModel()
    {
        NodeClickedCommand = new RelayCommand(NodeClicked);
        // ....
        // Some code that binds Nodes.
        // ....
    }

    private void NodeClicked()
    {
        MessageBox.Show("This is never shown");
    }
}
  

На этой странице у нас есть пользовательский элемент управления (CustomControl) и следующий xaml для привязки команды:

 <UserControl x:Class="ScreenView"
    x:Name="Screen"
    >
     <CustomControl Nodes="{Binding Nodes}">
                <CustomControl.ItemTemplate>
                    <DataTemplate>
                                <Button Command="{Binding ElementName=Screen,
                                     Path=DataContext.NodeClickedCommand}">
                                    <TextBlock>hello</TextBlock>
                                </Button>
                    </DataTemplate>
                </CustomControl.ItemTemplate>
        </CustomControl>
  

Наш пользовательский элемент управления SL использует приведенный выше шаблон (DataTemplate) для отображения его дочерних элементов:

 foreach(Node node in Nodes)
{
    FrameworkElement frameworkElement = (FrameworkElement)ItemTemplate.LoadContent();
    frameworkElement.DataContext = node ;
    this._canvas.Children.Add(frameworkElement);
}
  

Мы уверены, что:

  • ViewModel правильно привязан к View
  • Все узлы отображаются правильно
  • Обычная привязка работает корректно
  • В VS нет предупреждений о привязке
  • Привязка команды работает, если мы привязываем с помощью Command=»{Привязка NodeClickedCommand}», но, конечно, это привязывается к команде, которая должна существовать на одном узле, и мы хотим привязать к команде, которая существует в модели просмотра экрана.
  • Аналогичный сценарий работает со списком и ListBox.ItemTemplate

Проблема в том, что NodeClickedCommand никогда не привязывается, почему?

Ответ №1:

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

Вы написали, что ListBox работает с этой настройкой, поэтому попробуйте немного вникнуть в нее, чтобы увидеть, в какой момент она генерирует элементы, и убедитесь, что вы следуете аналогичному шаблону.

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

1. Я использовал reflector, чтобы посмотреть, как это делает ListBox, и я должен сказать, что через 30 минут я был полностью потерян. Похоже, они не используют DataTemplate. Метод LoadContent вообще.

2. Это метод PrepareContainerForItemOverride (см.: msdn.microsoft.com/en-us/library / … ) где и происходит все волшебство 🙂 AFAIK, вы также можете сделать element. ContentTemplate = ItemTemplate;

Ответ №2:

Это контейнер именования. Элементы управления ItemTemplates будут отображены элементом управления «позже» в визуальном цикле, поэтому область именования контейнера отличается от местоположения в UserControl. Следовательно, Screen не является допустимым элементом в области видимости.

Поскольку мы не видим внутреннюю работу вашего пользовательского элемента управления, мое лучшее решение для вас прямо сейчас — обернуть ваши узлы в отдельные модели представления и заставить эти модели представления ссылаться на NodeClickedCommand.

 public class NodeViewModel : BaseViewModel
{
   public Node Node { get; set; }
   public ICommand NodeClickedCommand { get; set; }
}

public class ScreenViewModel : BaseViewModel
{
    [NotifyPropertyChanged]
    public List<NodeViewModel> Nodes { get; set; }

    public ICommand NodeClickedCommand { get; set; }

    public ScreenViewModel()
    {
        NodeClickedCommand = new RelayCommand(NodeClicked);
        // ....
        // Some code that binds Nodes.
        // ....
        // This code here whatever it does, when it gets the list of 
        // nodes, wrap them inside a NodeViewModel instead like this

        var nvm = new NodeViewModel()
        {
            NodeClickedCommand = this.NodeClickedCommand,
            Node = Node
        };

        nodes.Add(nvm);
    }

    private void NodeClicked()
    {
        MessageBox.Show("This is never shown");
    }
}
  

Тогда ваш XAML будет выглядеть следующим образом:

 <UserControl x:Class="ScreenView"
    x:Name="Screen"
    >
     <CustomControl Nodes="{Binding Nodes}">
                <CustomControl.ItemTemplate>
                    <DataTemplate>
                                <Button Command="{Binding NodeClickedCommand}">
                                    <TextBlock>hello</TextBlock>
                                </Button>
                    </DataTemplate>
                </CustomControl.ItemTemplate>
        </CustomControl>
  

Вы все равно будете ссылаться на одну и ту же ICommand из ScreenViewModel, поэтому вы не создаете несколько экземпляров этой конкретной команды.

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

1. Это именно то, чего я пытаюсь избежать, я не хочу иметь ICommand на уровне узла.

Ответ №3:

Похоже, что использование ContentPresenter вместо ItemTemplate.LoadContent решает эту проблему:

 foreach(Node node in Nodes)
{
    ContentPresenter contentPresenter = new ContentPresenter();
    contentPresenter.Content = node;
    contentPresenter.ContentTemplate = ItemTemplate;
    this._canvas.Children.Add(contentPresenter);

//    FrameworkElement frameworkElement = (FrameworkElement)ItemTemplate.LoadContent();
//    frameworkElement.DataContext = node ;
//    this._canvas.Children.Add(frameworkElement);
}
  

Спасибо Дейну, поскольку он указал мне правильное направление.