-
Notifications
You must be signed in to change notification settings - Fork 36
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
Comments
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! |
@dhhunter, Even though within the /// <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 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. |
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. |
@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! |
@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! |
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
Severity (1-5)
3
Expected Behavior
Region.RegionManager should not be NULL within the
RegionBehavior
Thank you all for your great work!
The text was updated successfully, but these errors were encountered: