Где вы должны создавать экземпляры элементов управления пользовательского интерфейса из объектов данных, когда не подкласс ItemsControl?

Версия TLDR...

Мы пытаемся создать базовый подкласс Panel с наблюдаемым свойством Items. Затем элемент управления использует эти элементы данных для создания одного или нескольких связанных дочерних объектов пользовательского интерфейса для каждого элемента данных.

Естественно, нашей первой мыслью было просто создать подкласс ItemsControl, но это, похоже, не подходит, поскольку он использует ItemContainerGenerator, который генерирует только один "контейнер" на элемент, тогда как опять же, нам нужно потенциально создать несколько (которые в любом случае не являются контейнерами). Кроме того, все созданные элементы должны быть прямыми дочерними элементами на панели, а не храниться в контейнере, поэтому мы не можем пойти по пути шаблонов данных.

Таким образом, я просто использую стандартный элемент управления и пытаюсь найти подходящее место / событие, где я должен создавать / уничтожать результирующие дочерние элементы пользовательского интерфейса в ответ на изменения в коллекции Items.

Теперь подробности...

Обо всем по порядку. Если бы было что-то вроде ItemMultiContainerGenerator, это было бы идеально, но я не знаю ничего подобного.

Итак, просто следите за коллекцией на предмет изменений и поместите генерацию пользовательского интерфейса в событие CollectionChanged! Правильно? Это было наше первое предположение тоже. Проблема существует для каждого нового "Добавить" или "Удалить", мы должны прокрутить все существующие элементы управления, чтобы "дефрагментировать" определенные свойства индексации на них (подумайте в духе свойств Grid.Row или ZIndex), что означает, что если вы добавите десять предметов, вы запускаете дефрагментацию десять раз, а не один раз в конце.

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

Другая наша попытка заключалась в использовании MeasureOverride, поскольку он вызывался только один раз в ответ на вызов InvalidateMeasure, независимо от того, сколько дочерних элементов мы добавили или удалили. Проблемы (есть много) с этим подходом заключаются в том, что мы теряем контекст того, было ли что-то добавлено или удалено, что означает, что мы должны были отбросить всех детей и повторно добавить все новые, делая это крайне неэффективным. Кроме того, перебор с визуальным деревом или связывание настроек может привести к многократному выполнению прохода макета, так как мы меняли что-то, что влияет на макет, то есть на дочерние панели.

Я пытаюсь найти что-то, что происходит как часть общего процесса рендеринга (т. Е. С момента, когда элементу управления сообщается, что он недействителен, до тех пор, пока он не рендерится), но до вызова проходов Measure/Layout. Таким образом, я могу кэшировать операции добавления / удаления в событии CollectionChanged и просто пометить элемент управления как недействительный, дождаться этого загадочного события, затем обработать массовые изменения, затем отправить результаты в механизм компоновки и покончить с этим.

Используя Reflector, я попытался выяснить, где ItemsControl добавляет свои дочерние элементы на панель, но я не слишком далеко зашел, учитывая сложность сопряжения control/ItemContainerGenerator.

Итак, где же лучше всего создать / добавить элементы пользовательского интерфейса в элемент управления на основе изменений элементов данных?

1 ответ

Я думаю, что вам придется прослушивать изменения коллекции вручную. Есть несколько причин:

  • Изменение ваших визуальных детей приведет к недействительности вашего макета. Если вы поменяете своих детей по мере или в порядке, у вас будет бесконечный цикл.
  • Рендеринг происходит после макета. Меняя детей после макета, вы просите бесконечный цикл.

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

  • Реализовать кастом Collection что имеет поддержку AddRange а также RemoveRange (событие INotifyCollectionChanged поддерживает добавление или удаление нескольких элементов), поэтому вам не придется выполнять одну и ту же работу 10 раз для 10 новых элементов.
  • В обработчике изменений вашей коллекции используйте e.AddedItems и e.RemovedItems вместо доступа к базовой коллекции. Это предотвратит получение исключений из-за изменения коллекции во время ее перечисления.
  • Используйте BeginInvoke, чтобы предотвратить блокировку потока производителя при отправке обратно в поток пользовательского интерфейса.
  • Чтобы решить ваши проблемы инициализации, внедрить ISupportInitializeи используйте его, чтобы приостановить процесс "дефрагментации", если вам нужно добавить или удалить несколько элементов по одному. WPF автоматически добавит для вас начальные / конечные вызовы, когда ваш элемент управления будет создан в XAML.
  • Вытекают из FrameworkElement если ты не хочешь ControlTemplate, Понизьте накладные расходы.
  • Если это все еще не работает, потому что скорость, с которой ваша базовая коллекция изменяется слишком быстро, и дочерние элементы довольно просты, возможно, вы должны нарисовать их в OnRender

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

private DispatcherOperation pendingDefragOperation;

protected void ScheduleDefrag()
{
    if (pendingDefragOperation == null)
    {
        pendingDefragOperation = Dispatcher.BeginInvoke( 
            DispatcherPriority.Render, // You may want to play around with this
            new Action(Defrag));
    }
}

И вы бы назвали это на CollectionChanged, И в твоем Defrag позвони, ты бы поставил pendingDefragOperation к нулю.

Другие вопросы по тегам