Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Support heterogeneous item containers. #11068

Closed
wants to merge 1 commit into from

Conversation

grokys
Copy link
Member

@grokys grokys commented Apr 19, 2023

What does the pull request do?

As described in #9497 (comment) - we really need to support heterogeneous item containers, i.e. more than one type of item container in a single ItemsControl, with recycling.

This isn't supported by the WPF API that we moved to in #9677: although something can be hacked to work in WPF, it can never work with recycling, which means that as soon as we move to a WPF-like API, we now have to move away from it.

The main change in this PR is that the IsItemItsOwnContainer API has changed to the following:

ItemContainerType GetContainerTypeForItem(object? item)

Where ItemContainerType is an opaque type that represents the type of container desired by the ItemsControl for a particular item. There are two inbuilt ItemContainerTypes:

  • ItemContainerType.ItemIsOwnContainer signals that the item should be its own container, and as such as the replacement for the IsItemItsOwnContainer API
  • ItemContainerType.Default is returned by ItemControls which only support a single container

The new process for generating containers now goes like this:

  • GetContainerTypeForItem(object) is called to get the container type for the item. If the returned value is ItemContainerType.ItemIsOwnContainer then the item itself should be used as a container.
  • Otherwise the ItemContainerType returned from GetContainerTypeForItem(object) should be passed to CreateContainer(ItemContainerType) to create a new container.
  • PrepareItemContainer, AddInternalChild and ItemContainerPrepared should be called as before

When it comes time to recycle the container, it should be placed in a pool which is keyed on the ItemContainerType.

I chose to merge IsItemItsOwnContainer into GetContainerTypeForItem into a single method to avoid more virtual method calls than necessary when generating containers, even if this means we diverge more from the WPF API.

Feedback welcome!

Example

An example of a simple list box which supports multiple container types. First, the list box and the containers:

    public class ExampleListBox : ListBox, IStyleable
    {
        private readonly ItemContainerType _bar = new();

        Type IStyleable.StyleKey => typeof(ListBox);

        protected override ItemContainerType GetContainerTypeForItemOverride(object item)
        {
            return item is Foo ? ItemContainerType.Default : _bar;
        }

        protected override Control CreateContainerForItemOverride(ItemContainerType type)
        {
            if (type == _bar)
                return new BarContainer();
            return new FooContainer();
        }
    }

    public class FooContainer : Decorator
    {
        public FooContainer()
        {
            Child = new Ellipse
            {
                Width = 100,
                Height = 100,
                Fill = Brushes.Blue,
            };
        }
    }

    public class BarContainer : Decorator
    {
        public BarContainer()
        {
            Child = new Rectangle
            {
                Width = 100,
                Height = 100,
                Fill = Brushes.Green,
            };
        }
    }

Models:

    public class Foo { }
    public class Bar { }

Initialization:

            var items = Enumerable.Repeat<object>(new Foo(), 100).Concat(
                Enumerable.Repeat(new Bar(), 100))
                .ToList();

            // Shuffle items
            var random = new Random();

            for (var i = 0; i < items.Count; i++)
            {
                var j = random.Next(i, items.Count);
                var tmp = items[i];
                items[i] = items[j];
                items[j] = tmp;
            }

            Content = new ExampleListBox { ItemsSource = items };

Breaking changes

Changes an API that had already undergone a breaking change, so none technically.

@avaloniaui-team
Copy link
Contributor

You can test this PR using the following package version. 11.0.999-cibuild0033497-beta. (feed url: https://pkgs.dev.azure.com/AvaloniaUI/AvaloniaUI/_packaging/avalonia-all/nuget/v3/index.json) [PRBUILDID]

@billhenn
Copy link
Contributor

After reviewing the changes in the pull request, overall I like the direction it’s headed. The API updates in the PR allow for multiple known “container” control types to be used for an ItemsControl. And the ItemContainerType usage allows you to tackle the container recycling. The main caveat is that the ItemsControl developer needs to know the possible container types that can be used.

But what happens when the ItemsControl author doesn't know the container types that will be used?

WPF Technique to Pass the Item to GetContainerForItemOverride

Our WPF products also use the technique described in this comment under their WPF MenuEx.cs code sample:
#9497 (comment)

Augmenting that Technique with an ItemContainerTemplateSelector

Our new WPF Bars product makes extensive use of that technique and even augments it to use a WPF ItemContainerTemplateSelector (instead of creating known controls directly in code) so that any "container" control can be created for any view model item, and the consumers of our ItemsControls (our customers) are in full control over what kind of "container" is created, and without needing to inherit our ItemsControls and provide method overrides at all. It's an extremely flexible technique that is even demonstrated in WPF's native MenuItem control, but I don't believe it will work with the PR's API updates.

Native WPF MenuItem Example of This

Let me walk through how it works with MenuItem, since our Bars controls (ribbons, toolbars, menus) do the same general thing but on a larger basis. And everything I describe in this list is all natively in WPF:

  • MenuItem has a ItemContainerTemplateSelector property that gets/sets a ItemContainerTemplateSelector instance.
  • The ItemContainerTemplateSelector class is similar to a DataTemplateSelector class. Both have SelectTemplate methods that examine an object item and return a DataTemplate. The difference is the second parameters to the methods. DataTemplateSelector passes in the container control that will typically be the ContentPresenter instance showing the DataTemplate result. Whereas ItemContainerTemplateSelector passes in the parent ItemsControl instance.
  • A DataTemplateSelector is intended to provide the content via DataTemplate that will be displayed within something else, like a button or other ContentControl.
  • An ItemContainerTemplateSelector is intended to let you construct a UI control via a ItemContainerTemplate (which inherits DataTemplate) and that control will be instantiated via a `DataTemplate.LoadContent() method call.
  • See WPF's MenuItem.IsItemItsOwnContainer logic to see how they store the "_currentItem" similar to the MenuEx technique described in that other issue.
  • Then see WPF's MenuItem.GetContainerForItemOverride logic to see how they call into the ItemContainerTemplateSelector and create either a MenuItem or Separator from its resulting DataTemplate that the customer developer is able to configure.
  • Note that the MenuItem implementation only allows that MenuItem or Separator-based types can be used, but in reality, any control could be used in other situations.

Bars Controls Example

Now let's look at our Bars controls. We offer a StandaloneToolBar control for instance that uses this ItemContainerTemplate technique. If a customer binds a set of view models to the StandaloneToolBar.ItemsSource, and they have a customized ItemContainerTemplate set up to map each view model type to ItemContainerTemplate instances that generate appropriate "container" controls, they have complete control over the UI.

And best of all, the generated "container" controls are added directly as children of the ItemsControl's items panel, without the need for any useless ContentPresenter-like wrappers of any kind. This keeps the visual tree slimmer and makes things easier for custom ItemsControl items panels to examine children and perhaps take different layout actions based on what's there.

Here's an example of our main ItemContainerTempalteSelector-based class in a companion MVVM Library for our Bars product:
https://github.com/Actipro/WPF-Controls/blob/develop/Source/Bars.Mvvm/UI/Controls.Bars.Mvvm/TemplateSelectors/BarControlTemplateSelector.cs

And here is the XAML file that includes all the ItemContainerTemplates called into by the selector:
https://github.com/Actipro/WPF-Controls/blob/develop/Source/Bars.Mvvm/UI/Themes/BarsMvvmResourceDictionary.xaml

Summary

You can see how it's easy to set up any control to be bound to any view model with this technique. And it's fully extensible by the developer customer without them ever inheriting our controls and overriding methods.

It would be great if this kind of thing could be supported in Avalonia somehow too. We are happy to discuss further and answer any questions you have about our implementation.

@grokys
Copy link
Member Author

grokys commented Apr 21, 2023

Thanks for your feedback @billhenn - I'm going to think about this for a few days and tweak the API a little I hope.

@grokys
Copy link
Member Author

grokys commented Apr 26, 2023

I'm working on a new API, which will be best introduced in a separate PR, so I'll close this PR and link to it in the new PR for context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants