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

Region.RegionManager IS NULL within RegionBehavior #122

Open
dhhunter opened this issue Mar 9, 2024 · 6 comments
Open

Region.RegionManager IS NULL within RegionBehavior #122

dhhunter opened this issue Mar 9, 2024 · 6 comments
Assignees
Labels
needs-info Further information is requested

Comments

@dhhunter
Copy link

dhhunter commented Mar 9, 2024

Description

Region.RegionManager IS NULL within RegionBehavior

Steps to reproduce

I am trying to implement DependentViews and have run into problems because I am unable to access the Regions RegionManager while the RegionBehavior is running.

Environment

  • OS: Windows11
  • Prism.Avalonia Version: 8.1.97.11072
  • Avalonia Version: 11.2.999ci (Daily)

Severity (1-5)

3

Expected Behavior

Region.RegionManager should not be NULL within the RegionBehavior

public class DependentViewRegionBehavior :
    RegionBehavior
{
    public const string BehaviorKey = nameof(DependentViewRegionBehavior);

    private static readonly Dictionary<object, List<DependentViewInfo>> DependentViewCache = new();

    protected override void OnAttach()
    {
        Region.ActiveViews.CollectionChanged += Views_CollectionChanged;
    }

    private void Views_CollectionChanged(object? sender,
        NotifyCollectionChangedEventArgs eventArgs)
    {
        if (eventArgs.Action == NotifyCollectionChangedAction.Add)
            foreach (var newView in eventArgs.NewItems)
            {
                var viewList = new List<DependentViewInfo>();

                if (DependentViewCache.TryGetValue(
                        newView,
                        out var value))
                {
                    viewList = value;
                }
                else
                {
                    foreach (var attr in ReflectionHelpers
                                 .GetCustomAttributes<DependentViewAttribute>(newView.GetType()))
                    {
                        var dependentViewInfo = CreateDependentView(attr);

                        if (newView is ISupportDataContextSharing dcView &&
                            dependentViewInfo.View is ISupportDataContextSharing dcDependentView)
                            dcDependentView.DataContext = dcView.DataContext;

                        viewList.Add(dependentViewInfo);
                    }

                    DependentViewCache.TryAdd(newView, viewList);
                }

                //viewList.ForEach(x => regionManager
                //    .Regions[x.TargetRegionName]
                //    .Add(x.View));

                viewList.ForEach(x => Region
                    .RegionManager
                    .Regions[x.TargetRegionName]
                    .Add(x.View));
            }
        else if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
            foreach (var oldView in eventArgs.OldItems)
            {
                if (!DependentViewCache.ContainsKey(oldView)) continue;

                var viewList = DependentViewCache[oldView];

                viewList?.ForEach(x => Region
                    .RegionManager
                    .Regions[x.TargetRegionName]
                    .Remove(x.View));

                if (!ShouldKeepAlive(oldView))
                    DependentViewCache.Remove(oldView);
            }
    }

    private bool ShouldKeepAlive(object? oldView)
    {
        var lifetime = GetItemOrContextLifetime(oldView);

        if (lifetime is not null)
            return lifetime.KeepAlive;

        var lifetimeAttr = GetItemOrContextLifetimeAttribute(oldView);

        if (lifetimeAttr is not null)
            return lifetimeAttr.KeepAlive;

        return true;
    }

    private RegionMemberLifetimeAttribute? GetItemOrContextLifetimeAttribute(object? oldView)
    {
        var lifetimeAttr = ReflectionHelpers
            .GetCustomAttributes<RegionMemberLifetimeAttribute>(oldView.GetType())
            .FirstOrDefault();

        if (lifetimeAttr is not null)
            return lifetimeAttr;

        if (oldView is not UserControl element)
            return null;

        var dataContext = element.DataContext;

        if (dataContext is null)
            return null;

        var contextLifeTimeAttr = ReflectionHelpers
            .GetCustomAttributes<RegionMemberLifetimeAttribute>(dataContext.GetType())
            .FirstOrDefault();

        return contextLifeTimeAttr;
    }

    private IRegionMemberLifetime? GetItemOrContextLifetime(object? oldView)
    {
        if (oldView is IRegionMemberLifetime regionLifetime)
            return regionLifetime;

        if (oldView is UserControl { DataContext: IRegionMemberLifetime memberLifetime })
            return memberLifetime;

        return null;
    }

    private DependentViewInfo CreateDependentView(DependentViewAttribute attr)
    {
        var view = Activator.CreateInstance(attr.Type);

        return new DependentViewInfo(attr.TargetRegionName, view);
    }
}

Thank you all for your great work!

@dhhunter dhhunter added the enhancement New feature or request label Mar 9, 2024
@dhhunter
Copy link
Author

dhhunter commented Mar 10, 2024

This only throws an exception if you call RegionManager.RegisterViewWithRegion from App.OnInitialized with a view that has a DependentView, the RegionManager used to make the call does not have any Regions loaded yet...

In fact, I can take advantage of the RegionViewRegistry by changing my code to:

private void Views_CollectionChanged(object? sender,
    NotifyCollectionChangedEventArgs eventArgs)
{
    if (eventArgs.Action == NotifyCollectionChangedAction.Add)
        foreach (var newView in eventArgs.NewItems)
        {
            var viewList = new List<DependentViewInfo>();

            if (DependentViewCache.TryGetValue(
                    newView,
                    out var value))
            {
                viewList = value;
            }
            else
            {
                foreach (var attr in ReflectionHelpers
                             .GetCustomAttributes<DependentViewAttribute>(newView.GetType()))
                {
                    var dependentViewInfo = CreateDependentView(attr);

                    if (newView is ISupportDataContextSharing dcView &&
                        dependentViewInfo.View is ISupportDataContextSharing dcDependentView)
                        dcDependentView.DataContext = dcView.DataContext;

                    viewList.Add(dependentViewInfo);
                }

                DependentViewCache.TryAdd(newView, viewList);
            }

            if (Region.RegionManager is null)
            {
                viewList.ForEach(x => regionViewRegistry
                    .RegisterViewWithRegion(
                        x.TargetRegionName, 
                        () => x.View)
                );
            }
            else
            {
                viewList.ForEach(x => Region
                    .RegionManager
                    .Regions[x.TargetRegionName]
                    .Add(x.View));
            }
        }
    else if (eventArgs.Action == NotifyCollectionChangedAction.Remove)
        foreach (var oldView in eventArgs.OldItems)
        {
            if (!DependentViewCache.ContainsKey(oldView)) continue;

            var viewList = DependentViewCache[oldView];

            viewList?.ForEach(x => Region
                .RegionManager
                .Regions[x.TargetRegionName]
                .Remove(x.View));

            if (!ShouldKeepAlive(oldView))
                DependentViewCache.Remove(oldView);
        }
}

Thanks again for all of the great work!

@DamianSuess
Copy link
Collaborator

DamianSuess commented Apr 18, 2024

@dhhunter,
In your scenario, would it be helpful to override ConfigureDefaultRegionBehaviors(...) to register your DependentViewRegionBehavior?

Even though within the PrismApplicationBase that virtual method does get called before OnInitialized, the RegionBehavior class provides the method void OnAttach() which informs you when it has been attached to a region and the property bool IsAttached.

    /// <summary>Configures the <see cref="IRegionBehaviorFactory"/>. This will be the list of default behaviors that will be added to a region.</summary>
    protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors)
    {
        base.ConfigureDefaultRegionBehaviors(regionBehaviors);
        regionBehaviors.AddIfMissing<RegionBehaviors.DependentViewRegionBehavior>();
    }

Also, something sounded familiar with that class name. Is your issue similar to this video by Brian Lagunas?

https://www.youtube.com/watch?v=Y7P5T19uLtw

I hope this helps, without a full sample app, it's a little difficult to know if this is an implementation snafu or an issue.

@DamianSuess DamianSuess added needs-info Further information is requested and removed enhancement New feature or request labels Apr 18, 2024
@DamianSuess DamianSuess self-assigned this Apr 18, 2024
@dhhunter
Copy link
Author

dhhunter commented Apr 19, 2024

@DamianSuess Yes, I have been going through Brian's Prism Training and porting them to Avalonia! I am new to Avalonia, so you probably know a lot more about this than I do; however, it seems that Avalonia for lack of a better term lazily loads XAML so you can not be guaranteed that the Views/Regions will be available when the RegionBehaviors initialize? This requires a lot of convoluted code in the RegionBehaviors to allow them the wait for Avalonia to finish hooking everything up!

public class DependentViewRegionBehavior(IRegionViewRegistry regionViewRegistry) :
    RegionBehavior
{
    public const string BehaviorKey = nameof(DependentViewRegionBehavior);

    private static readonly Dictionary<object, List<DependentViewInfo>> DependentViewCache = new();

    protected override void OnAttach()
    {
        Region.ActiveViews.CollectionChanged += Views_CollectionChanged;

        if (Region.RegionManager is null)
            // Wait until we have the RegionManager
            Region.PropertyChanged += Region_PropertyChanged;
        else
            // Begin loading up the DependentViews for any Views already in the Region
            LoadDependentViews(Region.ActiveViews.ToList());
    }

    private void Views_CollectionChanged(
        object? sender,
        NotifyCollectionChangedEventArgs eventArgs)
    {
        if (eventArgs is { Action: NotifyCollectionChangedAction.Add, NewItems: not null })
            OnActiveViewsAdded(eventArgs.NewItems);
        else if (eventArgs is { Action: NotifyCollectionChangedAction.Remove, OldItems: not null })
            OnActiveViewsRemoved(eventArgs.OldItems);
    }

    private void Region_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName != nameof(Region.RegionManager) ||
            Region.RegionManager == null)
            return;

        // We now have the RegionManager, Unregister the Event
        Region.PropertyChanged -= Region_PropertyChanged;

        // Begin loading up the DependentViews for any Views already in the Region
        LoadDependentViews(Region.ActiveViews.ToList());
    }

    private void LoadDependentViews(IEnumerable newItems)
    {
        // We should have the RegionManager by now; however, the Regions we need may still not be available!
        foreach (var newView in newItems)
        {
            Debug.WriteLine($"Try Load: {newView}");

            var viewList = GetDependentViewsForView(newView);

            var groupedViewList = viewList.GroupBy(x => x.TargetRegionName);

            foreach (var regionDependentViewList in groupedViewList)
                if (Region.RegionManager.Regions.ContainsRegionWithName(regionDependentViewList.Key))
                {
                    // The Region Exists, and we can finally inject all the DependentViews for this Region!!!
                    LoadDependentViewsIntoRegion(newView, regionDependentViewList);
                }
                else
                {
                    // The Region does not yet exist, so we have to wait until it exists
                    void RegionsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
                    {
                        if (e is not { Action: NotifyCollectionChangedAction.Add, NewItems: not null })
                            return;

                        foreach (IRegion newRegion in e.NewItems)
                        {
                            if (newRegion.Name != regionDependentViewList.Key)
                                continue;

                            // The Region Exists, and we can finally inject the DependentViews for this Region!!!
                            LoadDependentViewsIntoRegion(newView, regionDependentViewList);

                            // Unregister the Event Handler
                            Region.RegionManager.Regions.CollectionChanged -= RegionsOnCollectionChanged;
                        }
                    }

                    Region.RegionManager.Regions.CollectionChanged += RegionsOnCollectionChanged;
                }
        }
    }

    private void OnActiveViewsAdded(IEnumerable newItems)
    {
        Debug.WriteLine($"{nameof(OnActiveViewsAdded)}()");

        if (Region.RegionManager is null)
            // We do not have a RegionManager, so we will come back to this later!
            return;

        // We have the RegionManager, so begin loading the DependentViews for newItems
        LoadDependentViews(newItems);
    }

    private void OnActiveViewsRemoved(IEnumerable oldItems)
    {
        Debug.WriteLine($"{nameof(OnActiveViewsRemoved)}()");

        foreach (var oldView in oldItems)
        {
            Debug.WriteLine($"Try Remove: {oldView}");

            if (!DependentViewCache.ContainsKey(oldView)) continue;

            var viewList = DependentViewCache[oldView];

            Debug.WriteLine($"Removing: {oldView}");

            foreach (var viewInfo in viewList)
                if (Region
                    .RegionManager
                    .Regions[viewInfo.TargetRegionName]
                    .Views
                    .Contains(viewInfo.View))
                    // We should always get here...
                    Region
                        .RegionManager
                        .Regions[viewInfo.TargetRegionName]
                        .Remove(viewInfo.View);
            // Bug?
            //viewList?.ForEach(x => Region
            //    .RegionManager
            //    .Regions[x.TargetRegionName]
            //    .Remove(x.View));

            if (!ShouldKeepAlive(oldView))
                DependentViewCache.Remove(oldView);
        }
    }

    private IEnumerable<DependentViewInfo> GetDependentViewsForView(object newView)
    {
        var viewList = new List<DependentViewInfo>();

        if (DependentViewCache.TryGetValue(
                newView,
                out var value))
        {
            viewList = value;
        }
        else
        {
            foreach (var attr in ReflectionHelpers
                         .GetCustomAttributes<DependentViewAttribute>(newView.GetType()))
            {
                var dependentViewInfo = CreateDependentView(attr);

                if (newView is ISupportDataContextSharing dcView &&
                    dependentViewInfo.View is ISupportDataContextSharing dcDependentView)
                    dcDependentView.DataContext = dcView.DataContext;

                viewList.Add(dependentViewInfo);
            }

            AddToDependentViewCache(newView, viewList);
        }

        return viewList;
    }

    private void LoadDependentViewsIntoRegion(
        object dependingView,
        IGrouping<string, DependentViewInfo> dependentViewsForRegion)
    {
        var region = Region.RegionManager.Regions[dependentViewsForRegion.Key];

        Debug.WriteLine($"Try Load: {dependingView}");

        foreach (var dependentView in dependentViewsForRegion)
        {
            Debug.WriteLine(
                $"Loading Depending View: {dependingView} into {dependentViewsForRegion.Key}, Parent = {dependingView}");

            if (!region.Views.Contains(dependentView.View))
                // We should always get here...
                region.Add(dependentView.View);
            // Bug?
        }
    }

    private void AddToDependentViewCache(
        object dependingView,
        IEnumerable<DependentViewInfo> dependentViewsForRegion)
    {
        if (DependentViewCache.ContainsKey(dependingView))
        {
            var currentDependentViews = DependentViewCache[dependingView];

            var combinedViewList = currentDependentViews.Concat(dependentViewsForRegion).ToList();

            DependentViewCache[dependingView] = combinedViewList;
        }
        else
        {
            DependentViewCache.Add(dependingView, [.. dependentViewsForRegion]);
        }
    }

    private bool ShouldKeepAlive(object? oldView)
    {
        var lifetime = GetItemOrContextLifetime(oldView);

        if (lifetime is not null)
            return lifetime.KeepAlive;

        var lifetimeAttr = GetItemOrContextLifetimeAttribute(oldView);

        if (lifetimeAttr is not null)
            return lifetimeAttr.KeepAlive;

        return true;
    }

    private RegionMemberLifetimeAttribute? GetItemOrContextLifetimeAttribute(object? oldView)
    {
        var lifetimeAttr = ReflectionHelpers
            .GetCustomAttributes<RegionMemberLifetimeAttribute>(oldView.GetType())
            .FirstOrDefault();

        if (lifetimeAttr is not null)
            return lifetimeAttr;

        if (oldView is not UserControl element)
            return null;

        var dataContext = element.DataContext;

        if (dataContext is null)
            return null;

        var contextLifeTimeAttr = ReflectionHelpers
            .GetCustomAttributes<RegionMemberLifetimeAttribute>(dataContext.GetType())
            .FirstOrDefault();

        return contextLifeTimeAttr;
    }

    private IRegionMemberLifetime? GetItemOrContextLifetime(object? oldView)
    {
        if (oldView is IRegionMemberLifetime regionLifetime)
            return regionLifetime;

        if (oldView is UserControl { DataContext: IRegionMemberLifetime memberLifetime })
            return memberLifetime;

        return null;
    }

    private DependentViewInfo CreateDependentView(DependentViewAttribute attr)
    {
        var view = Activator.CreateInstance(attr.Type);

        return new DependentViewInfo(attr.TargetRegionName, view);
    }
}

I have so far managed to port all of Brian's Advance Prism training over to Avalonia except for ScopedRegionManagers. Are you aware of the issue with ScopedRegionManagers or do I need to open a ticket on that too? It is probably also related to Avalonia processing XAML later.

@DamianSuess
Copy link
Collaborator

DamianSuess commented Apr 21, 2024

Brian's courses are a great start for sure!

Inherently there will be some minor differences between WPF and Avalonia; the same goes for Uno, MAUI, etc. I ran into the same thing creating the Prism Outlook'ish repo's tab support. Feel free to post a link to your repo for others to learn from it as well.

Your assumption is correct. Your recent code snippet does help show the case of the different platform implementations.

As far as the request for scoped regions it appears to be related to the following closed items:

p.s.
I made an aesthetic update to your recent post, adding syntax highlighting to make it easier to thumb through.

@dhhunter
Copy link
Author

dhhunter commented Apr 22, 2024

@DamianSuess Thanks for adding the syntax highlighting, it does make things much easier to read! I will mostly likely make my repo Public and post a link to it later, I am a perfectionist and I am not yet sure it is "perfect" yet lol

Those are related topics... I am attempting to port over Brian's ScopedRegions solution from his Mastering the TabControl course. Upon further investigation I think the problem is actually somewhere else in my code... Prism is so awesome when it just works; however, when it doesn't just work it really makes your head hurt!

@dhhunter
Copy link
Author

dhhunter commented Apr 25, 2024

@DamianSuess Woohoo! I was finally able to get Scoped Regions to work! The ScopedRegionManager does in fact "magically" find the new Regions in the View eventually; however, it does not automatically receive the original regions! It really is not that difficult to manually share the original regions with the ScopedRegionManager after it's created in the ScopedRegionNavigationContentLoader though, so it is not that big of a deal! Thanks again for all of the great work!

Sadly, having Regions inside of Regions really causes my DependentView's to go haywire :( However, at this point, I think maybe learning how to re-style controls in Avalonia might be a better use of my time... Do you happen to have any Avalonia Styling resources that you could recommend?

Edit: I ended up finding a bug in my TabControlRegionAdapter that was causing the DependentView's to go haywire! Woohoo!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-info Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants