Динамически установить UserControl как тело шаблона DataTemplate Listbox

У меня следующая установка:

<ListBox ItemSource="{Binding Targets}">
    <ListBox.ItemTemplate>
        <DataTemplate>
          <view:ViewName />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Я пытаюсь динамически решить, какое представление использовать во время выполнения, на основе свойства в DataContext ListBox. Проще говоря, я хочу заменить <view:ViewName> привязкой данных, которая возвращает правильное представление.

Я использую MEF для предоставления подключаемых модулей для моего приложения, которым может потребоваться предоставить настраиваемое представление для отображения элементов, когда это необходимо. Во время разработки я не знаю всех возможных типов представлений (они могут динамически загружаться из DLL), поэтому простой DataTemplateSelector не годится.

Я исследовал решения, но ничего не нашел.


person Dread Pirate Peter    schedule 26.03.2013    source источник
comment
согласны ли вы использовать какой-либо другой вспомогательный класс или аналогичный - если ваша модель представления - это только одно - и одно свойство - или несколько свойств в вашей виртуальной машине. Есть много решений для этого, я постараюсь написать что-нибудь позже - мне просто нужно знать, в чем цель. Например. см. это с flags решением (решение 2) stackoverflow.com/questions/15550344/ - (и вы можете сопоставить это не только с двумя вариантами и т. д.) - но есть способ получше, который я часто использую.   -  person NSGaga-mostly-inactive    schedule 26.03.2013
comment
У меня нет проблем с использованием хелпера / конвертера / и т. Д. Я просто могу использовать решение, которое требует, чтобы я перечислил все возможные варианты во время разработки в xaml, поскольку тогда я их не узнаю. Я думаю, самый простой способ продемонстрировать, что мне нужно, - это добавить гипотетический атрибут в DataTemplate как: <DataTemplate View="{Binding View}"/> где View - это свойство в DataTemplate для ListBox.   -  person Dread Pirate Peter    schedule 26.03.2013
comment
возможно, строка, содержащая имя представления. Я могу организовать, чтобы все представления находились в одном пространстве имен, думаю, даже динамические. И выше я имел в виду DataContext для ListBox, но у меня просто закончилось время редактирования   -  person Dread Pirate Peter    schedule 26.03.2013
comment
... но у вас есть все возможные шаблоны (для представлений), которые известны заранее, верно? то есть у вас есть конечное количество просмотров, которые вы планируете «задействовать» - на основе этой строки / идентификатора.   -  person NSGaga-mostly-inactive    schedule 26.03.2013
comment
Нет, здесь у меня проблемы. Я хочу иметь возможность динамически загружать новые классы действий через MEF, с которыми могут быть связаны новые представления. Во время выполнения шаги создаются из сценария конфигурации в XML, который ссылается на действие для каждого шага. Я хочу, чтобы третьи стороны в моей организации могли создавать новые действия без повторного выпуска основного программного обеспечения.   -  person Dread Pirate Peter    schedule 26.03.2013


Ответы (2)


Используя DataTemplateManager из этого сообщения Вы можете сделать что-то вроде:

DataTemplateManager.RegisterDataTemplate<ViewModelType1, ViewType1>();
DataTemplateManager.RegisterDataTemplate<ViewModelType2, ViewType2>();
DataTemplateManager.RegisterDataTemplate<ViewModelType3, ViewType3>();

тогда вы удалите ItemTemplate из ListBox:

<ListBox ItemSource="{Binding Targets}"/>

и в ViewModel ListBox вы можете:

public void AddTargets()
{
    Targets.Add(new ViewModelType1());
    Targets.Add(new ViewModelType2());
    Targets.Add(new ViewModelType3());
}

Затем каждый DataTemplate будет автоматически использоваться WPF для рендеринга каждой соответствующей ViewModel.

Также обратите внимание, что вы можете вызвать DataTemplateManager.RegisterDataTemplate() в любое время перед отображением ListBox, поэтому теоретически вы можете сделать это при загрузке частей MEF.

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

На основе вашего комментария вы можете создать один DataTemplate с ContentPresenter для отображения выбранного представления в соответствии со свойством в ViewModel:

<DataTemplate DataType="{x:Type local:TargetViewModel}">
    <ContentPresenter x:Name="MainContentPresenter" Content="{Binding}" ContentTemplate="{Binding YourProperty, Converter=SomeConverter}"/>

а внутри SomeConverter вы должны использовать ту же технику, что продемонстрирована в сообщении, для динамического создания DataTemplate.

person Federico Berasategui    schedule 26.03.2013
comment
Это потрясающе и так близко к тому, чем я действительно хочу заниматься. Я, наверное, смог бы с этим поработать, если бы пришлось. Но на самом деле я хочу выбрать свое представление на основе свойства в рамках одной модели представления, а не иметь отдельные модели представления для каждого представления. У меня есть TargetViewModel со свойством Action типа IAction. Отдельные конкретные классы, реализующие IAction, могут предоставить имя отображаемого представления. Итак, в идеальном мире ListBox будет использовать TargetViewModel.Action.View для определения представления для отображения. - person Dread Pirate Peter; 26.03.2013
comment
Это отлично сработало. Мне просто нужно было понять, что мне нужны отдельные модели представления для каждого действия. Я собирался столкнуться с дополнительными проблемами в будущем, пытаясь работать с конкретными классами Action через ссылку IAction. Таким образом, у меня есть модель представления с членом Concrete Action правильного типа, так что мое представление может получить необходимую информацию. Поставщик Action должен подтвердить модель представления сейчас, но я абстрагировал большую часть работы в базовом классе. Он отлично работает и является лучшим решением, чем то, что я искал изначально. Спасибо! - person Dread Pirate Peter; 27.03.2013

Поскольку вы хотите изменить шаблоны на основе привязанного значения, вы можете использовать DataTrigger для определения ContentTemplate ListBoxItem

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="ContentTemplate" Value="{StaticResource DefaultTemplate}"/>
    <Style.Triggers>
        <DataTrigger Property="{Binding SomeProperty}" Value="A">
            <Setter Property="ContentTemplate" Value="{StaticResource TemplateA}"/>
        </DataTrigger>
    </Style.Triggers>
</Style>

Я считаю, что это лучше, чем использование DataTemplateSelector, потому что оно переоценивается, если связанное свойство изменяется, а DataTemplateSelector - нет.

Если вы хотите изменить шаблоны на основе типа объекта, вы можете использовать Implicit DataTemplates. Это DataTemplates, которые определяют DataType, но не x:Key, и они будут использоваться каждый раз, когда WPF пытается нарисовать объект указанного типа.

Например, если у вас есть этот шаблон, определенный в вашем <X.Resources> где-то

<DataTemplate DataType="{x:Type models:ActionA}">
    <views:ActionAView />
</DataTemplate>

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

<ContentControl Content="{Binding SomeIActionObject}" />

<ItemsControl ItemsSource="{Binding CollectionOfIActionObjects}" />

Обновить

Вы упомянули, что позволите пользователям создавать модули с дополнительными шаблонами, которые импортируются с помощью MEF, поэтому в этом случае вам, вероятно, будет лучше использовать IValueConverter, которые ищут соответствующий шаблон в Application.Resources

Например, если связанное значение равно "A", тогда преобразователь может искать Application.Resources шаблон с именем "TemplateA" и возвращать его привязке

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="ContentTemplate" 
            Value="{Binding SomeProperty, 
                Converter={StaticResource MyTemplateConverter}}"/>
</Style>
person Rachel    schedule 26.03.2013
comment
Это действительно сработает очень хорошо, за исключением того, что я не знаю набор возможных значений во время разработки для создания статических ресурсов. - person Dread Pirate Peter; 26.03.2013
comment
@ user1394408 Тебе ведь нужно заранее создать свой Templates, не так ли? Вы не можете просто написать, если Property = Z, тогда используйте TemplateZ и не определите TemplateZ где-то. Я предполагаю, что вы могли бы использовать Converter вместо DataTrigger, хотя он будет смотреть на связанное свойство, а затем искать Application.Resources шаблон, который соответствует этому свойству, и возвращать его. - person Rachel; 26.03.2013
comment
то, что говорит @Rachel, имеет большой смысл - вам нужно что-то исправить в какой-то момент - или как вы планируете это сделать - person NSGaga-mostly-inactive; 26.03.2013
comment
Мой шаблон всегда представляет собой простую оболочку вокруг представления (содержащего UserControl). Представления будут динамической частью. Проблема в том, что они могут быть загружены динамически с помощью MEF, поэтому я бы не знал, что они собой представляют во время разработки. - person Dread Pirate Peter; 26.03.2013
comment
@ user1394408 В этом есть смысл. В этом случае я бы попытался использовать Converter для поиска шаблона, необходимого для привязанного значения, и возврата его в пользовательский интерфейс. См. Обновление моего вопроса :) - person Rachel; 26.03.2013
comment
Я выбрал решение DataTemplateManager ниже. Он отлично работает с MEF. Мне просто нужно было переосмыслить проблему (см. Мой комментарий ниже). Спасибо за помощь. - person Dread Pirate Peter; 27.03.2013
comment
@ user1394408 Нет проблем :) В качестве альтернативы DataTemplateManager вы также можете использовать Implicit DataTemplates, если вы основываете свое представление на типе объекта. Меньше кода :) - person Rachel; 27.03.2013