Prism 7 throws and exception when working with nested views

浪尽此生 提交于 2021-02-05 08:14:14

问题


I posted similar question few months ago Working with nested views using Prism with IsNavigationTarget which can return false, I'm still not sure what is the proper way to do it.

Imagine you have a view A, in this view A you have declared a region A, then you injected a view B into this region A. Similarly, in View B you have registered region B, and then you injected a view C into this region B. As it shown on the following picture:

In the ViewModelA for ViewA, I have a method SetUpSubViews() where I call:

_regionManager.RequestNavigate("regionA", "ViewB", NavigationCallback);

ViewModelB for View B implements INavigationAware. So that in OnNavigatedTo() method I call:

_regionManager.RequestNavigate("regionB", "ViewC", NavigationCallback);

ViewModelC for View C implements INavigationAware too.

Now, I have in both ViewModelB and ViewModelC in IsNavigationTarget() method:

public bool IsNavigationTarget(NavigationContext navigationContext)
    {            
        return false;
    }

It means I want to create new view everytime this view is navigated.

Both ViewB and ViewC implement IRegionMemberLifetime interface, where I set:

#region IRegionMemberLifetime

public bool KeepAlive => false;

#endregion

It means that I don't want to reuse view and I want it to be disposed.

And regions in the view are declared like this:

<ContentControl prism:RegionManager.RegionName="{x:Static localRegions:LocalRegions.RegionB}" />

Now, first time when I call SetUpSubViews() method on ViewModelA all is good. Second time when I call it I see the exception:

Region with the given name is already registered ...

What I need is to have a way to recreate view<->viewmodel pair from scratch everytime when I need. It seems when view is disposed, prism doesn't remove region that was declared in removed view. Question to community and prism developers, how to do it in proper way?

The current solution doesn't make happy, here is what I do: Step 1 - I set in ViewModelB and ViewModelC in INavigationAware part

    public bool IsNavigationTarget(NavigationContext navigationContext)
    {            
        return true;
    }

that signals to prism to do not create new views and probably it also means that if any region is found in the view to do not register it in the region manager.

Step 2 - When i need to inject view to region, i remove manually old view and create new one. So my SetUpSubViews() method looks like this:

protected void SetUpSubViews(){
            //get region by name
            var region = _regionManager.Regions["regionA"];
            // push to remove all views from the region
            region.RemoveAll();
            // navigate to view
           _regionManager.RequestNavigate("regionA", "ViewB", NavigationCallback);}

Similarly I have to remove ViewC from region regionB on ViewB. (here is region.RemoveAll() is a key line.)

Step3 - I don't implement IRegionMemberLifetime interface on viewB and viewC.

It works, but it doesn't look correct.

P.S. I also tried scoped manager, but i don't know how to propagate new created scoped manager to the viewmodels, cause they are created automatically, and if i resolve it via constructor I get main global manager instead of scoped.

Thanks.


回答1:


This is quite a troublesome problem. I recommend videos from Brian Lagunas himself where he provides a solution and explanation. For example this one. https://app.pluralsight.com/library/courses/prism-problems-solutions/table-of-contents

If you can watch it. If not I will try to explain.

The problem I believe is that IRegionManager from the container is a singleton and whenever you use it it is the same instance, so when you are trying to inject a region in an already injected region it will not work and you need to have a separate RegionManager for nested views.

This should fix it. Create two interfaces

public interface ICreateRegionManagerScope
{
    bool CreateRegionManagerScope { get; }
}
public interface IRegionManagerAware
{
    IRegionManager RegionManager { get; set; }
}

Create a RegionManagerAwareBehaviour

public class RegionManagerAwareBehaviour : RegionBehavior
{
    public const string BehaviorKey = "RegionManagerAwareBehavior";

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

    void Views_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            foreach (var item in e.NewItems)
            {
                IRegionManager regionManager = Region.RegionManager;

                // If the view was created with a scoped region manager, the behavior uses that region manager instead.
                if (item is FrameworkElement element)
                {
                    if (element.GetValue(RegionManager.RegionManagerProperty) is IRegionManager scopedRegionManager)
                    {
                        regionManager = scopedRegionManager;
                    }
                }

                InvokeOnRegionManagerAwareElement(item, x => x.RegionManager = regionManager);
            }
        }
        else if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            foreach (var item in e.OldItems)
            {
                InvokeOnRegionManagerAwareElement(item, x => x.RegionManager = null);
            }
        }
    }

    private static void InvokeOnRegionManagerAwareElement(object item, Action<IRegionManagerAware> invocation)
    {
        if (item is IRegionManagerAware regionManagerAwareItem)
        {
            invocation(regionManagerAwareItem);
        }

        if (item is FrameworkElement frameworkElement)
        {
            if (frameworkElement.DataContext is IRegionManagerAware regionManagerAwareDataContext)
            {
                // If a view doesn't have a data context (view model) it will inherit the data context from the parent view.
                // The following check is done to avoid setting the RegionManager property in the view model of the parent view by mistake.
                if (frameworkElement.Parent is FrameworkElement frameworkElementParent)
                {
                    if (frameworkElementParent.DataContext is IRegionManagerAware regionManagerAwareDataContextParent)
                    {
                        if (regionManagerAwareDataContext == regionManagerAwareDataContextParent)
                        {
                            // If all of the previous conditions are true, it means that this view doesn't have a view model
                            // and is using the view model of its visual parent.
                            return;
                        }
                    }
                }

                invocation(regionManagerAwareDataContext);
            }
        }
    }
}

Create ScopedRegionNavigationContentLoader

public class ScopedRegionNavigationContentLoader : IRegionNavigationContentLoader
{
    private readonly IServiceLocator serviceLocator;

    /// <summary>
    /// Initializes a new instance of the <see cref="RegionNavigationContentLoader"/> class with a service locator.
    /// </summary>
    /// <param name="serviceLocator">The service locator.</param>
    public ScopedRegionNavigationContentLoader(IServiceLocator serviceLocator)
    {
        this.serviceLocator = serviceLocator;
    }

    /// <summary>
    /// Gets the view to which the navigation request represented by <paramref name="navigationContext"/> applies.
    /// </summary>
    /// <param name="region">The region.</param>
    /// <param name="navigationContext">The context representing the navigation request.</param>
    /// <returns>
    /// The view to be the target of the navigation request.
    /// </returns>
    /// <remarks>
    /// If none of the views in the region can be the target of the navigation request, a new view
    /// is created and added to the region.
    /// </remarks>
    /// <exception cref="ArgumentException">when a new view cannot be created for the navigation request.</exception>
    public object LoadContent(IRegion region, NavigationContext navigationContext)
    {
        if (region == null) throw new ArgumentNullException("region");
        if (navigationContext == null) throw new ArgumentNullException("navigationContext");

        string candidateTargetContract = this.GetContractFromNavigationContext(navigationContext);

        var candidates = this.GetCandidatesFromRegion(region, candidateTargetContract);

        var acceptingCandidates =
            candidates.Where(
                v =>
                {
                    var navigationAware = v as INavigationAware;
                    if (navigationAware != null && !navigationAware.IsNavigationTarget(navigationContext))
                    {
                        return false;
                    }

                    var frameworkElement = v as FrameworkElement;
                    if (frameworkElement == null)
                    {
                        return true;
                    }

                    navigationAware = frameworkElement.DataContext as INavigationAware;
                    return navigationAware == null || navigationAware.IsNavigationTarget(navigationContext);
                });


        var view = acceptingCandidates.FirstOrDefault();

        if (view != null)
        {
            return view;
        }

        view = this.CreateNewRegionItem(candidateTargetContract);

        region.Add(view, null, CreateRegionManagerScope(view));

        return view;
    }

    private bool CreateRegionManagerScope(object view)
    {
        bool createRegionManagerScope = false;

        if (view is ICreateRegionManagerScope viewHasScopedRegions)
            createRegionManagerScope = viewHasScopedRegions.CreateRegionManagerScope;

        return createRegionManagerScope;
    }

    /// <summary>
    /// Provides a new item for the region based on the supplied candidate target contract name.
    /// </summary>
    /// <param name="candidateTargetContract">The target contract to build.</param>
    /// <returns>An instance of an item to put into the <see cref="IRegion"/>.</returns>
    protected virtual object CreateNewRegionItem(string candidateTargetContract)
    {
        object newRegionItem;
        try
        {
            newRegionItem = this.serviceLocator.GetInstance<object>(candidateTargetContract);
        }
        catch (ActivationException e)
        {
            throw new InvalidOperationException(
                string.Format(CultureInfo.CurrentCulture, "Cannot create navigation target", candidateTargetContract),
                e);
        }
        return newRegionItem;
    }

    /// <summary>
    /// Returns the candidate TargetContract based on the <see cref="NavigationContext"/>.
    /// </summary>
    /// <param name="navigationContext">The navigation contract.</param>
    /// <returns>The candidate contract to seek within the <see cref="IRegion"/> and to use, if not found, when resolving from the container.</returns>
    protected virtual string GetContractFromNavigationContext(NavigationContext navigationContext)
    {
        if (navigationContext == null) throw new ArgumentNullException(nameof(navigationContext));

        var candidateTargetContract = UriParsingHelper.GetAbsolutePath(navigationContext.Uri);
        candidateTargetContract = candidateTargetContract.TrimStart('/');
        return candidateTargetContract;
    }

    /// <summary>
    /// Returns the set of candidates that may satisfiy this navigation request.
    /// </summary>
    /// <param name="region">The region containing items that may satisfy the navigation request.</param>
    /// <param name="candidateNavigationContract">The candidate navigation target as determined by <see cref="GetContractFromNavigationContext"/></param>
    /// <returns>An enumerable of candidate objects from the <see cref="IRegion"/></returns>
    protected virtual IEnumerable<object> GetCandidatesFromRegion(IRegion region, string candidateNavigationContract)
    {
        if (region == null) throw new ArgumentNullException(nameof(region));
        return region.Views.Where(v =>
            string.Equals(v.GetType().Name, candidateNavigationContract, StringComparison.Ordinal) ||
            string.Equals(v.GetType().FullName, candidateNavigationContract, StringComparison.Ordinal));
    }
}

In your App.xaml

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
 containerRegistry.RegisterSingleton<IRegionNavigationContentLoader,ScopedRegionNavigationContentLoader>();
    
}

protected override void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors)
    {
       base.ConfigureDefaultRegionBehaviors(regionBehaviors);

       regionBehaviors.AddIfMissing(RegionManagerAwareBehaviour.BehaviorKey, typeof(RegionManagerAwareBehaviour));
    }

Coming to the finish. Now in your ViewModelB implement IRegionManagerAware and have it as a normal property

public IRegionManager RegionManager { get; set; }

Then at your ViewB implement ICreateRegionManagerScope and have it as a get property

public bool CreateRegionManagerScope => true;

Now it should work.

Again I truly recommend the videos at Pluralsight from Brian on Prism. He has a couple of videos that help a lot when you are starting with a Prism.



来源:https://stackoverflow.com/questions/63641883/prism-7-throws-and-exception-when-working-with-nested-views

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!