Prism中Region(上)
概述
在介紹Region之前我們首先有必要了解什麼是Region(區域)簡單來說,Region可以理解為View
<ContentControl prism:RegionManager.RegionName="MenuRegion" />
在後面我們就可以根據需要為這個名稱為MenuRegion的區域注入真正地View物件,這樣做的目的是為了真正地實現介面的解耦,這個是整個Region設計的核心思想。
原始碼分析
1 IRegion介面
按照我們之前讀原始碼系列的思路,我們首先還是從最基本的介面進行一個個分析。
/// <summary> /// Defines a model that can be used to compose views. /// </summary> public interface IRegion : INavigateAsync, INotifyPropertyChanged { /// <summary> /// Gets a readonly view of the collection of views in the region. /// </summary> /// <value>An <see cref="IViewsCollection"/> of all the added views.</value> IViewsCollection Views { get; } /// <summary> /// Gets a readonly view of the collection of all the active views in the region. /// </summary> /// <value>An <see cref="IViewsCollection"/> of all the active views.</value> IViewsCollection ActiveViews { get; } /// <summary> /// Gets or sets a context for the region. This value can be used by the user to share context with the views. /// </summary> /// <value>The context value to be shared.</value> object Context { get; set; } /// <summary> /// Gets the name of the region that uniquely identifies the region within a <see cref="IRegionManager"/>. /// </summary> /// <value>The name of the region.</value> string Name { get; set; } /// <summary> /// Gets or sets the comparison used to sort the views. /// </summary> /// <value>The comparison to use.</value> Comparison<object> SortComparison { get; set; } ///<overloads>Adds a new view to the region.</overloads> /// <summary> /// Adds a new view to the region. /// </summary> /// <param name="view">The view to add.</param> /// <returns>The <see cref="IRegionManager"/> that is set on the view if it is a <see cref="DependencyObject"/>. It will be the current region manager when using this overload.</returns> IRegionManager Add(object view); /// <summary> /// Adds a new view to the region. /// </summary> /// <param name="view">The view to add.</param> /// <param name="viewName">The name of the view. This can be used to retrieve it later by calling <see cref="GetView"/>.</param> /// <returns>The <see cref="IRegionManager"/> that is set on the view if it is a <see cref="DependencyObject"/>. It will be the current region manager when using this overload.</returns> IRegionManager Add(object view, string viewName); /// <summary> /// Adds a new view to the region. /// </summary> /// <param name="view">The view to add.</param> /// <param name="viewName">The name of the view. This can be used to retrieve it later by calling <see cref="GetView"/>.</param> /// <param name="createRegionManagerScope">When <see langword="true"/>, the added view will receive a new instance of <see cref="IRegionManager"/>, otherwise it will use the current region manager for this region.</param> /// <returns>The <see cref="IRegionManager"/> that is set on the view if it is a <see cref="DependencyObject"/>.</returns> IRegionManager Add(object view, string viewName, bool createRegionManagerScope); /// <summary> /// Removes the specified view from the region. /// </summary> /// <param name="view">The view to remove.</param> void Remove(object view); /// <summary> /// Removes all views from the region. /// </summary> void RemoveAll(); /// <summary> /// Marks the specified view as active. /// </summary> /// <param name="view">The view to activate.</param> void Activate(object view); /// <summary> /// Marks the specified view as inactive. /// </summary> /// <param name="view">The view to deactivate.</param> void Deactivate(object view); /// <summary> /// Returns the view instance that was added to the region using a specific name. /// </summary> /// <param name="viewName">The name used when adding the view to the region.</param> /// <returns>Returns the named view or <see langword="null"/> if the view with <paramref name="viewName"/> does not exist in the current region.</returns> object GetView(string viewName); /// <summary> /// Gets or sets the <see cref="IRegionManager"/> that will be passed to the views when adding them to the region, unless the view is added by specifying createRegionManagerScope as <see langword="true" />. /// </summary> /// <value>The <see cref="IRegionManager"/> where this <see cref="IRegion"/> is registered.</value> /// <remarks>This is usually used by implementations of <see cref="IRegionManager"/> and should not be /// used by the developer explicitly.</remarks> IRegionManager RegionManager { get; set; } /// <summary> /// Gets the collection of <see cref="IRegionBehavior"/>s that can extend the behavior of regions. /// </summary> IRegionBehaviorCollection Behaviors { get; } /// <summary> /// Gets or sets the navigation service. /// </summary> /// <value>The navigation service.</value> IRegionNavigationService NavigationService { get; set; } }
在我們瞭解IRegion內部關聯的物件之前,我們來看看IRegion介面繼承了哪些介面,首先就是INavigateAsync介面,初一看這個介面不太清楚什麼意思,但是我們可以通過類比瀏覽器的導航功能做一個對比,然後我們結合IRegion介面中定義的Views和ActiveViews屬性我們便有一個清晰的認識就是一個IRegion中會有多個View我們可以通過傳入不同的URL從而控制當前的Region中到底顯示的是哪一個介面?這個場景是不是非常常見,想想一下我們的微信、QQ等軟體,左側是聯絡人,右邊是具體資訊介面,當我們點選左側不同聯絡人時右側的Region區域會顯示不同的詳細資訊,而這些不同資訊都是在一個Region中進行展示的,有了這個解釋是不是非常形象。
1.1 INavigateAsync介面
/// <summary>
/// Provides methods to perform navigation.
/// </summary>
/// <remarks>
/// Convenience overloads for the methods in this interface can be found as extension methods on the
/// <see cref="NavigationAsyncExtensions"/> class.
/// </remarks>
public interface INavigateAsync
{
/// <summary>
/// Initiates navigation to the target specified by the <see cref="Uri"/>.
/// </summary>
/// <param name="target">The navigation target</param>
/// <param name="navigationCallback">The callback executed when the navigation request is completed.</param>
/// <remarks>
/// Convenience overloads for this method can be found as extension methods on the
/// <see cref="NavigationAsyncExtensions"/> class.
/// </remarks>
void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback);
/// <summary>
/// Initiates navigation to the target specified by the <see cref="Uri"/>.
/// </summary>
/// <param name="target">The navigation target</param>
/// <param name="navigationCallback">The callback executed when the navigation request is completed.</param>
/// <param name="navigationParameters">The navigation parameters specific to the navigation request.</param>
/// <remarks>
/// Convenience overloads for this method can be found as extension methods on the
/// <see cref="NavigationAsyncExtensions"/> class.
/// </remarks>
void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback, NavigationParameters navigationParameters);
}
IRegion實現的第二個介面是INotifyPropertyChanged介面,這個就不用做過多的解釋主要就是屬性變更通知UI的,這個就在這裡不做過多解釋。
在瞭解完了IRegion繼承的外部介面之前我們先來了解其內如關聯的相關介面
1.2 IViewsCollection介面
/// <summary>
/// Defines a view of a collection.
/// </summary>
public interface IViewsCollection : IEnumerable<object>, INotifyCollectionChanged
{
/// <summary>
/// Determines whether the collection contains a specific value.
/// </summary>
/// <param name="value">The object to locate in the collection.</param>
/// <returns><see langword="true" /> if <paramref name="value"/> is found in the collection; otherwise, <see langword="false" />.</returns>
bool Contains(object value);
}
這個是用在IRegion介面中的Views和ActiveViews屬性的,我們剛才說過一個IRegion中包含多個View物件並且通過實現INavigateAsync介面來實現不同的檢視物件的切換,這裡通過介面定義我們大概就瞭解其內部的實現,圍繞著這個 IViewsCollection物件我們發現在IRegion介面中關於View的Add、Remove、Active、DeActive都是和這個直接相關的,通過介面定義我們就能夠對整個Region包含的功能有一個清晰的認知。
1.3 IRegionManager介面
這個我們將會在後面的文章中就這個做單獨的分析,這裡我們需要注意的是一個IRegion對應一個IRegionManager,一個IRegionManager用來管理當前IRegion中的各種View物件,本節我們不重點去介紹這個部分。
1.4 IRegionBehavior介面
這個我們也將會在後面的文章中就這個做單獨的分析,這個介面主要是為當前的IRegion新增各種各種的Behavior,通過實現這個介面我們能為當前Region新增各種各樣的行為而且程式碼結構上會更加統一和標準,這個也是Prism框架設計的優秀地方。
/// <summary>
/// Interface for allowing extensible behavior on regions.
/// </summary>
public interface IRegionBehavior
{
/// <summary>
/// The region that this behavior is extending.
/// </summary>
IRegion Region { get; set; }
/// <summary>
/// Attaches the behavior to the specified region.
/// </summary>
void Attach();
}
1.5 IRegionNavigationServicer介面
在上面的分析中我們發現IRegionNavigationService這個介面主要是實現IRegion中Views實現導航功能,這個IRegionNavigationService這個介面也實現了INavigateAsync這個介面,所以我們可以猜測IRegion中的導航功能最終是通過IRegion內部關聯的
IRegionNavigationService介面實現的,而且IRegionNavigationService這個介面內部包含導航時的各種技術細節,並且通過時間向外通知當前導航的狀態,這個部分後面我們也將分章節去介紹這個部分的具體實現。
/// <summary>
/// Provides navigation for regions.
/// </summary>
public interface IRegionNavigationService : INavigateAsync
{
/// <summary>
/// Gets or sets the region owning this service.
/// </summary>
/// <value>A Region.</value>
IRegion Region { get; set; }
/// <summary>
/// Gets the journal.
/// </summary>
/// <value>The journal.</value>
IRegionNavigationJournal Journal { get; }
/// <summary>
/// Raised when the region is about to be navigated to content.
/// </summary>
event EventHandler<RegionNavigationEventArgs> Navigating;
/// <summary>
/// Raised when the region is navigated to content.
/// </summary>
event EventHandler<RegionNavigationEventArgs> Navigated;
/// <summary>
/// Raised when a navigation request fails.
/// </summary>
event EventHandler<RegionNavigationFailedEventArgs> NavigationFailed;
}
至此通過對IRegion介面中的分析,我們對於整個Region的功能有一個大概的瞭解,並對每一個關聯以及繼承的介面都有清晰的認知,我們先抓住整個脈絡從而對整體有一個清晰的認識,後面再深入細節,這樣我們理解整個框架就容易多了。
2 Region實現
這個部分看著程式碼很多,其實有了上面的分析你大概對這個部分的具體實現是怎麼樣的?而且我們最重要的是學習這些框架的封裝思想,這個才是最重要的。
/// <summary>
/// Implementation of <see cref="IRegion"/> that allows multiple active views.
/// </summary>
public class Region : IRegion
{
private ObservableCollection<ItemMetadata> _itemMetadataCollection;
private string _name;
private ViewsCollection _views;
private ViewsCollection _activeViews;
private object _context;
private IRegionManager _regionManager;
private IRegionNavigationService _regionNavigationService;
private Comparison<object> _sort;
/// <summary>
/// Initializes a new instance of <see cref="Region"/>.
/// </summary>
public Region()
{
Behaviors = new RegionBehaviorCollection(this);
_sort = DefaultSortComparison;
}
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Gets the collection of <see cref="IRegionBehavior"/>s that can extend the behavior of regions.
/// </summary>
public IRegionBehaviorCollection Behaviors { get; }
/// <summary>
/// Gets or sets a context for the region. This value can be used by the user to share context with the views.
/// </summary>
/// <value>The context value to be shared.</value>
public object Context
{
get => _context;
set
{
if (_context != value)
{
_context = value;
OnPropertyChanged(nameof(Context));
}
}
}
/// <summary>
/// Gets the name of the region that uniquely identifies the region within a <see cref="IRegionManager"/>.
/// </summary>
/// <value>The name of the region.</value>
public string Name
{
get => _name;
set
{
if (_name != null && _name != value)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.CannotChangeRegionNameException, _name));
}
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(Resources.RegionNameCannotBeEmptyException);
}
_name = value;
OnPropertyChanged(nameof(Name));
}
}
/// <summary>
/// Gets a readonly view of the collection of views in the region.
/// </summary>
/// <value>An <see cref="IViewsCollection"/> of all the added views.</value>
public virtual IViewsCollection Views
{
get
{
if (_views == null)
{
_views = new ViewsCollection(ItemMetadataCollection, x => true)
{
SortComparison = _sort
};
}
return _views;
}
}
/// <summary>
/// Gets a readonly view of the collection of all the active views in the region.
/// </summary>
/// <value>An <see cref="IViewsCollection"/> of all the active views.</value>
public virtual IViewsCollection ActiveViews
{
get
{
if (_views == null)
{
_views = new ViewsCollection(ItemMetadataCollection, x => true)
{
SortComparison = _sort
};
}
if (_activeViews == null)
{
_activeViews = new ViewsCollection(ItemMetadataCollection, x => x.IsActive)
{
SortComparison = _sort
};
}
return _activeViews;
}
}
/// <summary>
/// Gets or sets the comparison used to sort the views.
/// </summary>
/// <value>The comparison to use.</value>
public Comparison<object> SortComparison
{
get => _sort;
set
{
_sort = value;
if (_activeViews != null)
{
_activeViews.SortComparison = _sort;
}
if (_views != null)
{
_views.SortComparison = _sort;
}
}
}
/// <summary>
/// Gets or sets the <see cref="IRegionManager"/> that will be passed to the views when adding them to the region, unless the view is added by specifying createRegionManagerScope as <see langword="true" />.
/// </summary>
/// <value>The <see cref="IRegionManager"/> where this <see cref="IRegion"/> is registered.</value>
/// <remarks>This is usually used by implementations of <see cref="IRegionManager"/> and should not be
/// used by the developer explicitly.</remarks>
public IRegionManager RegionManager
{
get => _regionManager;
set
{
if (_regionManager != value)
{
_regionManager = value;
OnPropertyChanged(nameof(RegionManager));
}
}
}
/// <summary>
/// Gets the navigation service.
/// </summary>
/// <value>The navigation service.</value>
public IRegionNavigationService NavigationService
{
get
{
if (_regionNavigationService == null)
{
_regionNavigationService = ContainerLocator.Container.Resolve<IRegionNavigationService>();
_regionNavigationService.Region = this;
}
return _regionNavigationService;
}
set => _regionNavigationService = value;
}
/// <summary>
/// Gets the collection with all the views along with their metadata.
/// </summary>
/// <value>An <see cref="ObservableCollection{T}"/> of <see cref="ItemMetadata"/> with all the added views.</value>
protected virtual ObservableCollection<ItemMetadata> ItemMetadataCollection
{
get
{
if (_itemMetadataCollection == null)
{
_itemMetadataCollection = new ObservableCollection<ItemMetadata>();
}
return _itemMetadataCollection;
}
}
/// <overloads>Adds a new view to the region.</overloads>
/// <summary>
/// Adds a new view to the region.
/// </summary>
/// <param name="view">The view to add.</param>
/// <returns>The <see cref="IRegionManager"/> that is set on the view if it is a <see cref="DependencyObject"/>. It will be the current region manager when using this overload.</returns>
public IRegionManager Add(object view)
{
return this.Add(view, null, false);
}
/// <summary>
/// Adds a new view to the region.
/// </summary>
/// <param name="view">The view to add.</param>
/// <param name="viewName">The name of the view. This can be used to retrieve it later by calling <see cref="IRegion.GetView"/>.</param>
/// <returns>The <see cref="IRegionManager"/> that is set on the view if it is a <see cref="DependencyObject"/>. It will be the current region manager when using this overload.</returns>
public IRegionManager Add(object view, string viewName)
{
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.StringCannotBeNullOrEmpty, "viewName"));
}
return this.Add(view, viewName, false);
}
/// <summary>
/// Adds a new view to the region.
/// </summary>
/// <param name="view">The view to add.</param>
/// <param name="viewName">The name of the view. This can be used to retrieve it later by calling <see cref="IRegion.GetView"/>.</param>
/// <param name="createRegionManagerScope">When <see langword="true"/>, the added view will receive a new instance of <see cref="IRegionManager"/>, otherwise it will use the current region manager for this region.</param>
/// <returns>The <see cref="IRegionManager"/> that is set on the view if it is a <see cref="DependencyObject"/>.</returns>
public virtual IRegionManager Add(object view, string viewName, bool createRegionManagerScope)
{
IRegionManager manager = createRegionManagerScope ? this.RegionManager.CreateRegionManager() : this.RegionManager;
this.InnerAdd(view, viewName, manager);
return manager;
}
/// <summary>
/// Removes the specified view from the region.
/// </summary>
/// <param name="view">The view to remove.</param>
public virtual void Remove(object view)
{
ItemMetadata itemMetadata = this.GetItemMetadataOrThrow(view);
ItemMetadataCollection.Remove(itemMetadata);
if (view is DependencyObject dependencyObject && Regions.RegionManager.GetRegionManager(dependencyObject) == this.RegionManager)
{
dependencyObject.ClearValue(Regions.RegionManager.RegionManagerProperty);
}
}
/// <summary>
/// Removes all views from the region.
/// </summary>
public void RemoveAll()
{
foreach (var view in Views)
{
Remove(view);
}
}
/// <summary>
/// Marks the specified view as active.
/// </summary>
/// <param name="view">The view to activate.</param>
public virtual void Activate(object view)
{
ItemMetadata itemMetadata = this.GetItemMetadataOrThrow(view);
if (!itemMetadata.IsActive)
{
itemMetadata.IsActive = true;
}
}
/// <summary>
/// Marks the specified view as inactive.
/// </summary>
/// <param name="view">The view to deactivate.</param>
public virtual void Deactivate(object view)
{
ItemMetadata itemMetadata = this.GetItemMetadataOrThrow(view);
if (itemMetadata.IsActive)
{
itemMetadata.IsActive = false;
}
}
/// <summary>
/// Returns the view instance that was added to the region using a specific name.
/// </summary>
/// <param name="viewName">The name used when adding the view to the region.</param>
/// <returns>Returns the named view or <see langword="null"/> if the view with <paramref name="viewName"/> does not exist in the current region.</returns>
public virtual object GetView(string viewName)
{
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.StringCannotBeNullOrEmpty, "viewName"));
}
ItemMetadata metadata = this.ItemMetadataCollection.FirstOrDefault(x => x.Name == viewName);
if (metadata != null)
{
return metadata.Item;
}
return null;
}
/// <summary>
/// Initiates navigation to the specified target.
/// </summary>
/// <param name="target">The target.</param>
/// <param name="navigationCallback">A callback to execute when the navigation request is completed.</param>
public void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback)
{
this.RequestNavigate(target, navigationCallback, null);
}
/// <summary>
/// Initiates navigation to the specified target.
/// </summary>
/// <param name="target">The target.</param>
/// <param name="navigationCallback">A callback to execute when the navigation request is completed.</param>
/// <param name="navigationParameters">The navigation parameters specific to the navigation request.</param>
public void RequestNavigate(Uri target, Action<NavigationResult> navigationCallback, NavigationParameters navigationParameters)
{
this.NavigationService.RequestNavigate(target, navigationCallback, navigationParameters);
}
private void InnerAdd(object view, string viewName, IRegionManager scopedRegionManager)
{
if (this.ItemMetadataCollection.FirstOrDefault(x => x.Item == view) != null)
{
throw new InvalidOperationException(Resources.RegionViewExistsException);
}
ItemMetadata itemMetadata = new ItemMetadata(view);
if (!string.IsNullOrEmpty(viewName))
{
if (this.ItemMetadataCollection.FirstOrDefault(x => x.Name == viewName) != null)
{
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, Resources.RegionViewNameExistsException, viewName));
}
itemMetadata.Name = viewName;
}
if (view is DependencyObject dependencyObject)
{
Regions.RegionManager.SetRegionManager(dependencyObject, scopedRegionManager);
}
this.ItemMetadataCollection.Add(itemMetadata);
}
private ItemMetadata GetItemMetadataOrThrow(object view)
{
if (view == null)
throw new ArgumentNullException(nameof(view));
ItemMetadata itemMetadata = this.ItemMetadataCollection.FirstOrDefault(x => x.Item == view);
if (itemMetadata == null)
throw new ArgumentException(Resources.ViewNotInRegionException, nameof(view));
return itemMetadata;
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// The default sort algorithm.
/// </summary>
/// <param name="x">The first view to compare.</param>
/// <param name="y">The second view to compare.</param>
/// <returns></returns>
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "y")]
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "x")]
public static int DefaultSortComparison(object x, object y)
{
if (x == null)
{
if (y == null)
{
return 0;
}
else
{
return -1;
}
}
else
{
if (y == null)
{
return 1;
}
else
{
Type xType = x.GetType();
Type yType = y.GetType();
ViewSortHintAttribute xAttribute = xType.GetCustomAttributes(typeof(ViewSortHintAttribute), true).FirstOrDefault() as ViewSortHintAttribute;
ViewSortHintAttribute yAttribute = yType.GetCustomAttributes(typeof(ViewSortHintAttribute), true).FirstOrDefault() as ViewSortHintAttribute;
return ViewSortHintAttributeComparison(xAttribute, yAttribute);
}
}
}
private static int ViewSortHintAttributeComparison(ViewSortHintAttribute x, ViewSortHintAttribute y)
{
if (x == null)
{
if (y == null)
{
return 0;
}
else
{
return -1;
}
}
else
{
if (y == null)
{
return 1;
}
else
{
return string.Compare(x.Hint, y.Hint, StringComparison.Ordinal);
}
}
}
}
2.1 Region中Views的管理
這個部分我就不做每一部分程式碼詳細分析了,重點縷清楚其背後設計的思想。首先無論是Views還是ActiveViews程式碼中通過一個ObservableCollection
2.2 Region中的Context
在當前的Region中暫時沒看清楚這個Context的作用,不過其註釋其實說的很清楚就是為這些Views提供一個共享的資料上下文。
/// <summary>
/// Gets or sets a context for the region. This value can be used by the user to share context with the views.
/// </summary>
/// <value>The context value to be shared.</value>
public object Context
{
get => _context;
set
{
if (_context != value)
{
_context = value;
OnPropertyChanged(nameof(Context));
}
}
}
2.3 Region中的Navigation
IRegion中實現了INavigateAsync介面實現了View的導航功能,在Region的內部是通過一個前面分析過的IRegionNavigationService型別的NavigationService去實現的,其內部的具體原理後面分析具體程式碼的時候再進行講述。
2.4 Region中的SortComparison
我們在看Region這個部分的程式碼時我們發現,很多的篇幅是介紹同一個Region內部多個Views的排序規則的,這個主要是通過定義一個規則讓這些Views有一個先後順序,這個決定後面View載入的一些細節,甚至我們可以看到通過在View上面新增自定義屬性ViewSortHint我們能夠人為的對其先後順序進行排序,這個需要注意。
public static int DefaultSortComparison(object x, object y)
{
if (x == null)
{
if (y == null)
{
return 0;
}
else
{
return -1;
}
}
else
{
if (y == null)
{
return 1;
}
else
{
Type xType = x.GetType();
Type yType = y.GetType();
ViewSortHintAttribute xAttribute = xType.GetCustomAttributes(typeof(ViewSortHintAttribute), true).FirstOrDefault() as ViewSortHintAttribute;
ViewSortHintAttribute yAttribute = yType.GetCustomAttributes(typeof(ViewSortHintAttribute), true).FirstOrDefault() as ViewSortHintAttribute;
return ViewSortHintAttributeComparison(xAttribute, yAttribute);
}
}
}
總結
有了上面從IRegion介面的定義到IRegion介面的實現我們對整個Prism框架中的Region有一個清晰的認知,在後面的章節中我們將會對其中的技術細節,比如IRegionManager、IRegionNavigationService的具體實現分章節進行一一講述,力求將整個Prism框架中由外到內,由總體到區域性一一分析從而使自己有一個更加清晰的認知。