Привязка страницы в ItemsControl

#c# #wpf #binding

#c# #wpf #привязка

Вопрос:

Я пытаюсь создать окно, в котором отображается неизвестное количество страниц внутри на основе коллекции, переданной в builder.

Концепция показалась мне довольно простой — создайте страницу шаблона, создайте коллекцию страниц во время инициализации и отобразите их через ItemControl. Итак:

Класс контекста данных:

 class CollectionDetailsWindowContext
{
    public List<EntityDetailsPage> Pages { get; }

    public CollectionDetailsWindowContext(Collection _collection)
    {
        Pages = new List<EntityDetailsPage>();

        foreach (var entity in _collection.Entities)
            Pages.Add(new EntityDetailsPage(entity));
    }
}
  

XAML:

 <ItemsControl ItemsSource="{Binding Pages}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Frame Content="{Binding .}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>
  

Если я запускаю этот код, это приводит к следующему исключению

 System.InvalidOperationException: 'Page can have only Window or Frame as parent.'
  

Когда я пытаюсь выполнить все привязки вручную без ItemControl (таким образом, выбирая первые несколько объектов из коллекции и привязывая их к фреймам), все работает нормально, что наводит меня на мысль, что проблема в том, как ItemControl передает страницу во фрейм.

Ответ №1:

Хорошо, итак, вот пара вещей.

Во-первых, традиционный подход заключается в разделении данных и представления. Если вы загуглите «MVVM» или «MVC», вы увидите массу информации об этом. Лично я рекомендую MVVM. Таким образом, у вас обычно не было бы страницы, которая привязывается к другим страницам. Отображаемая страница («представление») должна привязываться к данным (ну, к «модели представления», представляющей данные), а не к другим страницам.

Во-вторых, страницы обычно отображаются во фреймах с использованием NavigateTo. Вы могли бы создать StackPanel, содержащую фреймы, а затем загружать страницу в каждый фрейм, но это кажется излишеством. Было бы проще просто иметь ListView и шаблон для деталей. Если у вас есть несколько типов сведений, вы могли бы использовать DataTemplateSelector в представлении списка.

В-третьих, в конечном итоге вы, вероятно, захотите обновить данные, для чего вам понадобится коллекция, которая уведомляет представление об изменениях в выбранном списке сведений об объектах. Это модель представления для списка. Также каждая сущность (EntityDetail в вашем случае) должна уведомлять о своих свойствах в потоке пользовательского интерфейса (или иметь отдельный объект модели представления, который это делает), но я не буду слишком углубляться в это здесь. Опять же, я говорю об обычном способе выполнения чего-либо, конечно, есть много других способов.

Итак … возможно, что-то вроде этого:

 partial class DetailsPage {
    public ObservableCollection<EntityDetail> EntityDetails {get;}

    public DetailsPage( ObservableCollection<EntityDetail> entityDetails) {
        this.EntityDetails = entityDetails;
    } 
}

<Page
    x:Class="DetailsPage"
    …
    >
    <Page.Resources>
        <ResourceDictionary>
            <DataTemplate x:Name="EntityDetailDataTemplate" x:DataType="local:EntityDetail">
                <TextBlock Text={x:Bind Name, Mode=OneWay}/> (assuming Name is a property in EntityDetail)
            </DataTemplate>
        </ResourceDictionary>
    </Page.Resources>
    <ListView
        ItemsSource={x:Bind EntityDetails, Mode=OneWay}
        ItemTemplate={StaticResource EntityDetailDataTemplate}
        …
        >
    </ListView>
</Page>
  

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

1. Спасибо, вы правы относительно подхода MVVM, и в конце дня я, вероятно, собираюсь переключиться на него. На данный момент, однако, моя основная проблема с использованием DataTemplates заключается в том, что страница для EntityDetails довольно сложная (1800-2000 строк xaml), и изменять ее без конструктора для наблюдения немедленных последствий изменений довольно хлопотно. Я хотел бы принять как ваш, так и mm8 ответ.

2. Существуют разные взгляды на то, что следует делать, когда что-то «немного перепутано» в чьем-то коде. Лично, если это существенное изменение, я предпочитаю исправить это заранее, а не отпускать. В противном случае я просто в конечном итоге копаю дыру глубже.

3. @JagdCrab, еще одна мысль. Если вы действительно хотите привязать к списку страниц, вы могли бы управлять из Frame, добавить свойство страницы и переходить на страницу всякий раз, когда она меняется. Тогда, по вашему мнению, вы могли бы использовать ListView и использовать этот класс, производный от фрейма, в DataTemplate.

Ответ №2:

…что наводит меня на мысль, что проблема заключается в том, как ItemControl передает страницу во фрейм.

Причина, по которой ваш код не работает, заключается в том, что ItemTemplate не применяется к UIElements таким Pages как.

Вы могли бы исправить это, создав свой собственный ItemsControl и переопределив IsItemItsOwnContainerOverride метод:

 public class FrameItemsControl : ItemsControl
{
    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return false;
    }
}
  

Возвращается реализация по умолчанию (item is UIElement) .

Использование:

 <local:FrameItemsControl ItemsSource="{Binding Pages}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <WrapPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Frame Content="{Binding .}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</local:FrameItemsControl>
  

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

1. @JagdCrab: Вы попробовали мое предложение?