构建模块化 Silverlight 应用程序






4.90/5 (10投票s)
本文介绍了一种灵活实用的可重用控件,该控件对于模块化 Silverlight 应用程序至关重要。它有助于改善大型 Silverlight 应用程序的组合结构和运行时性能。
引言
在运行时 延迟加载 代码包(按需加载功能代码模块/程序集)在中到大型 Silverlight RIA 中可以减小初始下载大小。这可以确保更快的启动性能和更清晰的代码结构。本文介绍的 Silverlight Module 方法(Silverlight Module 是绑定在一个二进制包文件中的功能集合。它不同于 .NET Module,后者是一个编译单元)使开发人员能够并行处理不同的功能组。它还促进了物理上和逻辑上模块化应用程序的创建,这些应用程序可以在运行时按需单独加载并随后呈现。
有几篇文章讨论了按需包交付的机制,包括 Silverlight 如何:按需程序集部署、按需下载内容 和 Silverlight 中动态内容交付的管理(作者:Dino Esposito)。这里提出的模块化 Silverlight 应用程序方法利用了相同的底层机制;它还内置了错误处理、状态消息、自动呈现和其他实用集成功能的机制,因此开发人员可以更专注于模块逻辑而不是底层实现。
您可以查看 示例模块化 Silverlight 应用程序,其中包含完整的源代码。
什么是 Silverlight 模块
“模块”概念源自 Adobe Flex。它是一个独立的、可部署的二进制包,按需加载到主应用程序中。模块的一个显著特点是它仅在需要时下载。典型的模块在应用程序的上下文中运行,而一个模块又可以引用其他模块作为子模块。
当一个大型单体应用程序导致启动或运行时出现不期望的延迟时,模块化就变得必要。这通常是由于内存使用量高,并且随着越来越多的功能被打包进来,这种情况通常会随着时间的推移而加剧。由于初始下载大小变大,设计时可维护性和运行时性能都会受到影响。
模块化可以减少启动时出现的不期望的延迟,它允许应用程序的各个部分(模块)从网络下载,并随后以逻辑块(即模块)的形式加载。通常,应用程序在启动时不需要启用所有功能。此外,并非所有用户在会话期间都需要应用程序的所有功能。
模块通过封装相关功能来提高应用程序中相关功能的高级内聚性。模块仅在用户需要与其交互时加载。某些功能是条件性需要的:代码根据业务逻辑动态确定加载哪些模块。
在设计时,模块将内聚的功能组合在一起。由于它们与主应用程序分离,不同的团队成员可以并行开发和测试不同的模块。在重新构建应用程序时,只需要重新编译已更改的模块,而不是整个应用程序。
在运行时,模块化可以缩短启动时间,因为应用程序 XAP 更小。内存使用量得到改善,因为只加载引用的模块。性能更佳,并且可以利用浏览器缓存重新加载模块。
Silverlight 模块具有上述所有特性和优势。它被创建为一个没有应用程序入口点的 Silverlight 应用程序。因此,它无法独立于应用程序运行。该模块被编译成 XAP 包。
构建 Silverlight 模块的技巧
构建模块化 Silverlight 应用程序的关键是使用本文提供的 SilverModule
控件。我们建议开发人员遵循某些准则来构建模块化 Silverlight 应用程序。
- 将应用程序分解为模块:要考虑的因素包括模块大小、模块内的内聚性、可重用性、业务需求等。
- 每个模块都需要对 SilverModule 程序集进行静态引用,作为模块加载器、可视化渲染器和错误处理程序,仅在运行时需要加载子模块时使用。
- 每个模块都会有其公开的自定义类型,该类型需要从
UserControl
派生。派生的自定义类型可以根据需要设计为实现一个公共接口。实际上,模块接口将是应用程序特定的。SilverModule
可以轻松地扩展以支持公共接口。 - 模块需要被创建为一个常规的 Silverlight 应用程序(可以删除应用程序入口点),Visual Studio 会将其编译成 XAP 格式。请注意,XAP 文件本质上是一个 ZIP 文件。它通常比未压缩的程序集文件小。
要动态加载 Silverlight 模块,调用代码片段会创建一个 SilverModule
实例,并将其 ModuleName
设置为要加载的模块的名称。SilverModule
控件此外还提供了灵活性,当需要更细粒度的控件时,例如模块 URL、自定义类型名称、程序集名称等。从应用程序的角度来看,SilverModule
控件提供以下功能:
- 它可以在 XAML 中作为常规 User Control 实例化。
- 它内置了逻辑,可以根据模块名称推断默认模块属性。
- 它会自动从远程服务器下载二进制 XAP 包。
- 它报告下载进度和(任何)错误。
- 它自动从下载的二进制流中提取程序集,从模块中实例化自定义类型,并自动将该实例添加到其可视化容器中,使其呈现。
- 它处理和报告下载、提取、实例化和呈现过程中的错误。
- 当需要替换现有模块时,它会清理可视化容器。
- 它提供了相同的机制来处理子模块(当一个模块需要将其他模块加载到自身中时)。
我们为本文提供了一个 Visual Studio 2008 SP1 解决方案,用于 演示 SilverModule
控件如何帮助构建模块化 Silverlight 应用程序。
使用 SilverModule 控件
SilverModule
是一个可以通过 XAML 实例化的 Silverlight User Control。它使用 WebClient
异步方法下载 XAP 二进制包,使用 StreamResourceInfo
提取程序集,并通过反射从加载的模块中实例化自定义类型。这些机制已在 Silverlight 中的动态内容交付管理,第一部分 中进行了讨论。我们将更专注于 SilverModule
添加的增强部分,以简化延迟加载并提供错误处理和进度报告。
SilverModule
被创建为一个 Silverlight 类库,可以静态链接到另一个库、模块或应用程序。当将该库添加到应用程序的引用中并指定了程序集命名空间后,就可以在 XML 中轻松实例化它。
<UserControl x:Class="SilverModuleDemo.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ext="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
xmlns:sm="clr-namespace:SilverModule;assembly=SilverModule"
Loaded="UserControl_Loaded">
<!--other XAML markup is omitted-->
<sm:SilverModule x:Name="moduleLoader"
ModuleName="{Binding Path=SelectedModuleName}" />
</UserControl>
在最简单的情况下,只需设置 ModuleName
属性。SilverModule
的代码隐藏将尝试从 ModuleName
推断模块的 URL、程序集名称和自定义类型名称,假设它们都相同(扩展名和命名空间名称不同;详情请参见下载的代码)。例如,在随附的演示项目中,模块名称是“SilverModuleTestOne
”。模块 URL 解析为“SilverModuleTestOne.xap”。模块内的程序集名称假定为“SilverModuleTestOne.dll”。并且,自定义类型名称从模块名称推断为“SilverModuleTestOne.SilverModuleTestOne
”。另一方面,这些属性也可以在 XAML 中设置(参见图 1b)。它们由 SilverModule
控件内的所有四个相应的依赖属性处理。
<!--other XAML markup is omitted-->
<sm:SilverModule x:Name="moduleLoader" ModuleRelativePath="."
ModuleAssemblyName="SilverModuleTestOne.dll"
ModuleTypeName="SilverModuleTestOne.SilverModuleTestOne"
ModuleName="{Binding Path=SelectedModuleName}" />
要在您的项目中使用的 SilverModule
控件,以便在运行时动态按需下载模块,以上所示的内容几乎就是您需要做的全部。您当然需要构建模块并与应用程序一起部署它们,但在深入研究构建模块的细节之前,让我们先看看 SilverModule
控件内部究竟有什么。
SilverModule 控件内部
SilverModule
控件派生自 UserControl
。其 XAML 仅设置了“Loaded
”事件处理程序,并具有一个空的 Grid
布局控件。
<UserControl x:Class="SilverModule.SilverModule"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Loaded="OnSilverModuleLoaded">
<Grid x:Name="LayoutRoot" Background="Transparent">
</Grid>
</UserControl>
SilverModule
公开以下**可绑定**属性:ModuleName
、ModuleRelativePath
、ModuleAssemblyName
和 ModuleTypeName
。只有 ModuleName
是必需的:如果省略了其他三个属性,它们的值将从 ModuleName
推断(稍后会详细介绍)。
当 ModuleName
属性设置为目标模块名称时,该模块将从服务器检索并加载到内存中。为了实现这一点,SilverModule
使用了一个名为 ModuleMaker
的辅助类。当 ModuleMaker
执行时,它会将状态消息传播给其调用者(在本例中,它是 SilverModule XAML 的代码隐藏,即 SilverModule.xaml.cs)。ModuleMaker
还处理下载和实例化过程中发生的错误。我们将在后面的部分详细介绍 ModuleMaker
。
public partial class SilverModule : UserControl
{
private ModuleMaker _moduleMaker;
// UI element that displays downloading progress and error message
private TextBlock _loaderTextBox;
public SilverModule()
{
InitializeComponent();
InitSilverModule();
}
private void InitSilverModule()
{
_moduleMaker = new ModuleMaker();
_moduleMaker.ModuleContentReady +=
new EventHandler<modulereadyeventargs>(onModuleContentReady);
LayoutRoot.DataContext = _moduleMaker;
if (null == _statusTextBlock)
{
//create the instance of TextBlock that shows the downloading process
_statusTextBlock = new TextBlock();
_statusTextBlock.TextAlignment = TextAlignment.Center;
_statusTextBlock.VerticalAlignment = VerticalAlignment.Center;
_statusTextBlock.Foreground =
new SolidColorBrush(Color.FromArgb(0xFF, 0x88, 0, 0));
//Create the binding description
_statusTextBlock.SetBinding(TextBlock.TextProperty,
new Binding("StatusMessage"));
}
}
private void onModuleContentReady(object s, ModuleReadyEventArgs e)
{
//display the loaded module content
LayoutRoot.Children.Clear();
LayoutRoot.Children.Add(e.ModuleContent);
}
private void OnSilverModuleLoaded(object sender, RoutedEventArgs e)
{
InitStatusTextBlock();
}
private void InitStatusTextBlock()
{
LayoutRoot.Children.Add(_statusTextBlock);
}
图 2b 显示了 InitSilverModule
如何创建 ModuleMaker
的实例,然后设置 _statusTextBlock
,以便它接收来自 ModuleMaker
的异步进度消息。请注意,_statusTextBlock
的文本已绑定到 ModuleMaker
类属性 StatusMessage
。由于 Grid
实例 LayoutRoot
的 DataContext
设置为 ModuleMaker
实例 _moduleMaker
,下载的进度通知和错误消息会自动显示在 TextBox
_statusTextBlock
上。创建 TextBlock
控件而不是在 XAML 中创建的原因是,它需要在下载过程中(或发生错误时)动态添加到 LayoutRoot
,并且在下载的模块准备好呈现时需要从 LayoutRoot
中替换。
在 SilverModule
加载后,会调用 InitStatusTextBlock
方法。它将 _statusTextBlock
添加到 SilverModule
页面的 LayoutRoot
中。
InitSilverModule
方法还设置了 ModuleContentReady
的事件处理程序。当模块已从服务器检索并随后被实例化(重建)时,ModuleMaker
会引发 ModuleContentReady
事件。事件处理程序 onModuleContentReady
将新创建的模块添加到 SilverModule
的 LayoutRoot
中,通过用新创建的目标模块替换 _statusTextBlock
。
private void onModuleContentReady(object sender, ModuleReadyEventArgs e)
{
//display the loaded module content
LayoutRoot.Children.Clear();
LayoutRoot.Children.Add(e.ModuleContent);
}
public void SetModuleName(string newValue)
{
if (LayoutRoot.Children.Count > 0)
{//already had a module loaded, remove it first
LayoutRoot.Children.Clear();
InitLoaderText();
}
//this property setter will start the module downloading automatically
_mStatus.ModuleName = newValue;
}
此时,没有发生错误,并且模块已自动呈现到布局中。模块功能已动态加载并准备好与最终用户进行交互。
现在,让我们检查一下启动模块下载的机制。当 SilverModule
的可绑定 ModuleName
属性被设置为某个值时,它会调用 SetModuleName
。当 ModuleName
更改时,会引发回调 OnModuleNamePropertyChanged
事件,事件处理程序将调用 SetModuleName
方法。
#region Bindable ModuleName Property
/// <summary>
/// The assembly name only, no extension (.xap, or .dll),
/// MUST be set before using other properties
/// Also assuming the package name is [ModuleName].xap and
/// it locates at the same folder as where the loading Silverlight app (main xap) is
/// Also assuming the UserControl Type name implemented
/// inside [ModuleName] is also the same as [ModuleName]
/// </summary>
public string ModuleName
{
get { return (string)GetValue(ModuleNameProperty); }
set { SetValue(ModuleNameProperty, value); }
}
// Using a DependencyProperty as the backing store for ModuleName.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty ModuleNameProperty =
DependencyProperty.Register("ModuleName", typeof(string), typeof(SilverModule),
new PropertyMetadata("", new PropertyChangedCallback(OnModuleNamePropertyChanged)));
// DP: changed callback
private static void OnModuleNamePropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
string newValue = (string)e.NewValue;
if (String.IsNullOrEmpty(newValue))
return;
SilverModule ctrl = (SilverModule)d;
ctrl.SetModuleName(newValue);
}
#endregion
接下来,我们将看到当 SetModuleName
执行时会发生什么。
public void SetModuleName(string newValue)
{
if (LayoutRoot.Children.Count > 0)
{
LayoutRoot.Children.Clear();
InitStatusTextBlock();
}
//this property setter will start the module downloading automatically
_moduleMaker.ModuleName = newValue;
}
如上面图 2e 所示,状态将被重置,并且 ModuleMaker
的 ModuleName
属性将被设置为新的模块名称(newValue
)。这会启动下载和加载机制,该机制将在下一节中讨论,届时我们将讨论 ModuleMaker
的内部工作原理。
模块状态和错误处理
模块状态和在下载、提取、实例化过程中的错误处理由 ModuleMaker
类负责。ModuleMaker
类具有以下职责:
- 从服务器下载模块(通常是 XAP 文件)
- 从 XAP 二进制流中提取模块程序集
- 实例化一个模块自定义类型实例
- 准备好呈现时引发
ModuleContentReady
事件 - 报告下载进度和(任何)错误
public class ModuleMaker : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public event EventHandler<modulereadyeventargs>
ModuleContentReady = delegate { };
private WebClient _webClient = null;
private string _statusMessage = "";
public string StatusMessage
{
get { return _statusMessage; }
set {
_statusMessage = value;
PropertyChanged(this,
new PropertyChangedEventArgs("StatusMessage"));
}
}
private string _moduleName;
public string ModuleName
{
get
{
return _moduleName;
}
set
{
if (String.IsNullOrEmpty(value))
throw new ArgumentNullException("ModuleName should " +
"never be empty or null!");
if (value != _moduleName)
{
//new module name is set, needs to start download
_moduleName = value;
StartToDownloadModule();
}
}
}
…
public string Error
{
set
{
this.StatusMessage = String.Format("Failed to load {0}: {1}",
ModuleName, value);
}
}
private void StartToDownloadModule()
{
if (null == _webClient)
{
//initialize the downloader
_webClient = new WebClient();
_webClient.DownloadProgressChanged +=
new DownloadProgressChangedEventHandler(onDownloadProgressChanged);
_webClient.OpenReadCompleted +=
new OpenReadCompletedEventHandler(onOpenReadCompleted);
}
if (_webClient.IsBusy)
{
//needs to cancel the previous loading
this.StatusMessage = "Cancelling previous downloading...";
_webClient.CancelAsync();
return;
}
Uri xapUrl = new Uri(this.ModuleURL, UriKind.RelativeOrAbsolute);
_webClient.OpenReadAsync(xapUrl);
}
private void onDownloadProgressChanged(object sender,
DownloadProgressChangedEventArgs e)
{
LoadingProcess = e.ProgressPercentage;
}
private void onOpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
if (null != e.Error)
{
//report the download error
this.Error = e.Error.Message;
return;
}
if (e.Cancelled)
{
// cancelled previous downloading, needs to re-start the loading
StartToDownloadModule();
return;
}
// Load a particular assembly from XAP
Assembly aDLL = GetAssemblyFromPackage(this.ModuleAssemblyName, e.Result);
if (null == aDLL)
{
//report the assembly extracting error
this.Error = "Module downloaded but failed to extract the assembly." +
" Please check the assembly name.";
return;
}
// Get an instance of the XAML object
UserControl content = aDLL.CreateInstance(this.ModuleTypeName) as UserControl;
if (null == content)
{
//report the type instnatiating error
this.Error = "Module downloaded and the assembly extracted” +
“ but failed to instantiate the custome type.”
“ Please check the type name.";
return;
}
//tell the event handler it's ready to display
ModuleContentReady(this,
new ModuleReadyEventArgs() { ModuleContent = content } );
}
}
每当 ModuleName
更改时,StartToDownloadModule
方法(图 3)就会执行。它确保创建一个 WebClient
的单个实例,并连接 DownloadProgressChanged
和 OpenReadComplete
事件。在调用 OpenReadAsync
异步下载二进制 XAP 包之前,它会确保取消任何先前的下载,因为一个 WebClient
实例只能异步下载一个对象。
DownloadProgressChanged
事件处理程序只需获取整数百分比值,然后将其设置为 LoadingProcess
属性(参见图 3)。LoadingProcess
属性将构建一个用户友好的字符串消息,然后将其设置为 StatusMessage
。由于 StatusMessage
已绑定到动态 TextBlock
的 Text
属性,因此每当 StatusMessage
值更改时,数据绑定引擎都会自动更新 TextBlock
。当模块包需要一些时间下载时,这种进度更新很有用,并且用户可以看到它在运行时更新。
一个值得注意的技巧是,新的模块下载(由 ModuleName
属性更改触发)实际上是如何开始的:当 StartToDownloadModule
(图 3)方法第二次调用时,它实际上就开始下载了。因为当 StartToDownloadModule
第一次运行时,它检测到 WebClient
仍在忙碌,它会简单地调用 CancelAsyn
然后返回。(图 3)。当处理 OpenReadComplete
事件时(图 3),它会检查取消是否完成。如果完成,则会再次调用 StartToDownloadModule
。当 StartToDownloadModule
第二次运行时,WebClient
实例不再忙碌(已取消),并且 OpenReadAsyn
最终被执行。
图 3 还显示了错误是如何处理的。当发生下载错误时,由于 ModuleName
错误、服务器上不存在该模块包,或者 URL 错误等,WebClient
实例将引发一个 OpenReadCompleted
事件,并将 Error
属性设置为非 null。事件处理程序在执行任何其他任务之前始终会检查 Error
属性;如果设置了,它会将 Error.Message
传递给 StatusMessage
属性,然后显示给用户。
当模块成功下载后,GetAssemblyFromPackage
将尝试从下载的二进制流中提取程序集。当流出现错误,或者 ModuleAssemplyName
错误(或者包中不存在该名称的程序集)时,GetAssemblyFromPackage
将返回 null
,并将字符串“模块已下载但无法提取程序集。请检查程序集名称。”设置为 StatusMessage
以显示错误。
在下载和程序集提取都成功后,代码将继续从提取的程序集中实例化自定义类型。如果程序集中不存在该名称的自定义类型,或者该类型未从 UserControl
派生,则会向用户显示以下错误消息:“模块已下载且程序集已提取,但无法实例化自定义类型。请检查类型名称。”
当没有发生错误时,将有一个指向自定义类型实例的引用,SilverModuleStatus
将引发一个名为 ModuleContentReady
的自定义事件。在构造 SilverModule
时设置了事件处理程序,并且该事件由 SilverModule
代码隐藏类中的 onModuleContentReady
方法处理。同样,onModuleContentReady
方法将清理 LayoutRoot
;如果之前加载了模块,它将与 TextBlock
一起从可视化树中移除。加载模块中的自定义类型实例将在 LayoutRoot
中呈现。
关于演示项目
解决方案中的 SilverModuleDemo 项目是主应用程序。它演示了如何下载新模块(SilverModuleTestOne)并将其渲染在右侧窗格中,如何下载的新模块(SilverModuleTestTwo)替换右侧窗格中现有的模块,以及用户如何请求加载子模块(SilverModuleTestSub)到另一个模块中。此外,它还显示了如何处理错误(请求不存在的模块)。
SilverModuleDemo:这是主要的演示应用程序。它有一个列表框,其中包含以下字符串项:“SilverModuleTestOne”、“SilverModuleTestTwo”、“Non-exist Module”。当用户单击第一项“SilverModuleTestOne”时,SilverModuleTestOne 模块将被加载并在右侧面板上呈现。当用户单击第二项“SilverModuleTestTwo”时,SilverModuleTestTwo 模块将被加载并在右侧面板上呈现。当用户单击第三项“Non-exist Module”时,将显示一条消息,因为找不到要查找的模块。简要说明各种模块如下:
- SilverModuleTestOne:这是一个动态模块,它只显示一个
TextBlock
。 - SilverModuleTestTwo:这是一个动态模块,它显示一个按钮,上面显示单词“Load”。当用户单击“Load”按钮时,SilverModuleTestSub 模块将作为子模块加载。
- SilverModuleTestSub:当用户从 SilverModuleTestTwo 模块单击加载按钮时,此模块将被加载。
- SilverModule:如前所述,这是负责动态加载和呈现其他模块的所有工作的类库。SilverModule 在此处实现。
如果您要创建的模块打算仅在 Shell 应用程序中使用,而不是独立使用,您可以简单地从 App.XML 中删除 Appcalition_Startup
事件处理程序。演示解决方案中的所有三个模块项目都遵循相同的做法:Application_Startup
和 Application_Exit
事件处理程序都已注释掉,因此它们在直接从页面引用时无法独立运行。
为了利用仅基于 ModuleName
的默认“推断”逻辑,演示解决方案中的所有三个模块项目还故意将生成的 Page
类型重命名为与项目名称相同的名称,然后推断出的 ModuleTypeName
值将是正确的。由于“添加 Silverlight 应用程序”对话框中的默认目标文件夹值将确保模块的 XAP 输出被复制到与 Shell 应用程序相同的 ClientBin 文件夹中,因此无需在 XAML 中设置 ModuleRelativePath
属性。默认情况下,程序集名称与项目名称相同,因此您也可以省略 ModuleAssemblyName
属性以使用其推断的值。因此,在 Shell 应用程序(SilverModuleDemo)的 Page.XAML 中,<sm:SilverModule>
标签可以保持最简单的形式(图 1)。只需设置 ModuleName
属性即可。您可以尝试在 XAML 中设置其他属性,如图 2 所示,看看它的反应。
当演示解决方案运行时,选择第一个项将导致 ModuleName
属性通过数据绑定而更改。最终,SilverModuleStatus
类将开始下载选定的模块。您可以在 SilverModuleStatus
中设置断点,以逐步了解其执行过程。当下载、提取和实例化全部成功时,按需加载的模块将显示在右侧窗格中。当选择第二项时,新模块将发生类似的过程。新下载的模块将有效地替换以前下载的模块。在您不希望新模块内容替换现有模块的用例中,您可以简单地在 Shell 应用程序的 XAML 中添加多个 <sm:SilverModule>
标签,然后每个标签将在其 ModuleName
正确设置时加载一个模块。
顺便说一下,模块不仅可以被 Shell 应用程序加载,模块还可以加载其他模块(我们称之为子模块),方法相同。为了演示这一点,演示解决方案中的 SilverModuleTestTwo 项目引用了 SilverModule 库,并在其 XAML 中有一个 <sm:SilverModule>
来设置在按需加载子模块(当单击“Load”按钮时)。子模块的实现方式与常规模块相同。请查看 SilverModuleTestSub 项目了解详情。
结论
SilverModule
提供了一种低开销但实用的解决方案,用于构建模块化 Silverlight 应用程序。其内置的推断逻辑、自动下载、提取、实例化、呈现以及进度/错误处理机制使得构建模块化 Silverlight 应用程序更加容易。
模块概念足够强大,可以提高大型 RIA 的内聚性、封装性、运行时性能、内存使用量,并最终改善客户体验。这种方法有利于团队环境中的快速开发。对子模块的支持使得在保持逻辑关系的同时,可以将功能组进一步细分。
历史
- 2009.01.16 - 初稿。
- 2009.02.05 - 准备审查。