Custome StackPanel Prism RegionAdapter to support Ordering

264 views Asked by At

I have the following implementation of RegionAdapter for a StackPanel but I need strict ordering of items I associate with a region can anyone help?

I want Views that Register themselves to the Region to be able to control there position maybe an index number of some sort

    protected override void Adapt(IRegion region, StackPanel regionTarget)
    {
        region.Views.CollectionChanged += (sender, e) =>
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (FrameworkElement element in e.NewItems)
                    {
                        regionTarget.Children.Add(element);
                    }

                    break;

                case NotifyCollectionChangedAction.Remove:
                    foreach (UIElement elementLoopVariable in e.OldItems)
                    {
                        var element = elementLoopVariable;
                        if (regionTarget.Children.Contains(element))
                        {
                            regionTarget.Children.Remove(element);
                        }
                    }

                    break;
            }
        };
    }
3

There are 3 answers

0
Marc On BEST ANSWER

How to tackle this greatly depends on whether the sorting refers to (a) the type of the view or (b) to the instance of the view. The former would be the case if you only wanted to specify that for example Views of type ViewA should be above Views of type ViewB. The latter is the case if you want to specify how several concrete instances of the same view type are sorted.

A. Sort type wise

On option is to implement a custom attribute, something like OrderIndexAttribute, which exposes an integer property:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public OrderIndexAttribute : Attribute
{
    public int Index { get; }

    public OrderIndexAttribute(int index)
    {
        Index = index;
    }
}

Mark your view class with that attribute:

[OrderIndex(2)]
public ViewA : UserControl
{...}

Get the attribute of the type when adding the view to the region:

case NotifyCollectionChangedAction.Add:
    foreach (FrameworkElement element in e.NewItems)
    {
        // Get index for view
        var viewType = element.GetType();
        var viewIndex= viewType.GetCustomAttribute<OrderIndexAttribute>().Index;
        // This method needs to iterate through the views in the region and determine
        // where a view with the specified index needs to be inserted
        var insertionIndex = GetInsertionIndex(viewIndex);
        regionTarget.Children.Insert(insertionIndex, element);
    }
    break;

B. Sort instance wise

Make your views implement an interface:

public interface ISortedView 
{
   int Index { get; }
}

On adding the view to the region, try casting the inserted view to the interface, read the index and then do the same as above:

case NotifyCollectionChangedAction.Add:
    foreach (FrameworkElement element in e.NewItems)
    {
        // Get index for view
        var sortedView = element as ISortedView;
        if (sortedView != null)
        {
            var viewIndex = sortedView.Index;
            // This method needs to iterate through the views in the region and determine
            // where a view with the specified index needs to be inserted
            var insertionIndex = GetInsertionIndex(viewIndex);
            regionTarget.Children.Insert(insertionIndex, sortedView);
        }
        else
        { // Add at the end of the StackPanel or reject adding the view to the region }
    }
0
Marco G On

The answer from Marc and R.Evans helped me to create my own a little more generic RegionAdapter with the following improvements:

  • uses ViewSortHint to be compatible with Prism 6
  • Prism 7 / .Net 5 compatible
  • Helper Class for use in multiple Adapters
  • less code

Adapt method:

    protected override void Adapt(IRegion region, StackPanel regionTarget)
    {
        region.Views.CollectionChanged += (s, e) =>
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
                foreach (FrameworkElement item in e.NewItems)
                {
                    regionTarget.Children.Insert(regionTarget.Children.GetInsertionIndex(item), item);
                }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
                foreach (UIElement item in e.OldItems)
                {
                    if (regionTarget.Children.Contains(item))
                    {
                        regionTarget.Children.Remove(item);
                    }
                }
        };
    }

Helper/Extension:

internal static int GetInsertionIndex(this IList items, in object newItem)
    {
        // Return the list index of the viewIndex
        foreach (object item in items)
        {
            var currentIndex = item.GetType().GetCustomAttribute<ViewSortHintAttribute>()?.Hint ?? "0";
            var intendedIndex = newItem.GetType().GetCustomAttribute<ViewSortHintAttribute>()?.Hint ?? "0";

            if (currentIndex.CompareTo(intendedIndex) >= 0)
                return items.IndexOf(item);
        }

        // if no greater index is found, insert the item at the end
        return items.Count;
    }
0
Revans611 On

I found Marc's "A. Sort type wise" case to be very helpful for my situation. I needed to sort the views into the region by using the OrderIndexAttribute and still be able to add a view if it did not actually have the OrderIndexAttribute.

As you will see below, I did this by keeping track of the view indexes in a List. The insertion index of a view without the attribute is defaulted to zero so that it sorts to the front(or top) of the StackPanel.

This original post is rather old, but maybe someone will stumble upon it as I did and will find my contribution to be helpful. Refactoring suggestions are welcome. :-)

public class StackPanelRegionAdapter : RegionAdapterBase<StackPanel>
{
    private readonly List<int> _indexList;

    public StackPanelRegionAdapter(IRegionBehaviorFactory behaviorFactory)
        : base(behaviorFactory)
    {
        _indexList = new List<int>();
    }

    protected override void Adapt(IRegion region, StackPanel regionTarget)
    {
        
        region.Views.CollectionChanged += (sender, e) =>
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (FrameworkElement element in e.NewItems)
                    {
                        var viewType = element.GetType();

                        // Get the custom attributes for this view, if any
                        var customAttributes = viewType.GetCustomAttributes(false);

                        var viewIndex = 0;  // Default the viewIndex to zero

                        // Determine if the view has the OrderIndexAttribute.
                        // If it does have the OrderIndexAttribute, get its sort index.
                        if (HasThisAttribute(customAttributes))
                        {
                            viewIndex= viewType.GetCustomAttribute<OrderIndexAttribute>().Index;
                        }
                        
                        // Get the insertion index
                        var insertionIndex = GetInsertionIndex(viewIndex);

                        regionTarget.Children.Insert(insertionIndex, element);
                    }
                    break;

                case NotifyCollectionChangedAction.Remove:
                    foreach (UIElement elementLoopVariable in e.OldItems)
                    {
                        var element = elementLoopVariable;
                        if (regionTarget.Children.Contains(element))
                        {
                            regionTarget.Children.Remove(element);
                        }
                    }
                    break;
            }
        };
    }

    private static bool HasThisAttribute(IReadOnlyList<object> customAttributes)
    {
        // Determine if the view has the OrderIndexAttribute
        if (customAttributes.Count == 0) return false;
        for (var i = 0; i < customAttributes.Count; i++)
        {
            var name = customAttributes[i].GetType().Name;
            if (name == "OrderIndexAttribute") return true;
        }

        return false;
    }

    private int GetInsertionIndex(in int viewIndex)
    {
        // Add the viewIndex to the index list if not already there
        if (_indexList.Contains(viewIndex) == false)
        {
            _indexList.Add(viewIndex);
            _indexList.Sort();
        }

        // Return the list index of the viewIndex
        for (var i = 0; i < _indexList.Count; i++)
        {
            if (_indexList[i].Equals(viewIndex))
            {
                return i;
            }
        }

        return 0;
    }

    protected override IRegion CreateRegion()
    {
        return new AllActiveRegion();
    }
}