Composite UI Application Block (CAB) 向导
展示了一种在 CAB 中创建向导的非常简单的方法。
引言
在本文中,我将展示一个相对简单的 CAB 向导实现。我在我工作的公司做过类似的事情,并认为它可能对其他有类似需求的人有所帮助。
我假设您了解 CAB 的基础知识,这是一个非常基础的向导实现,我不会深入探讨复杂程度很高的 CAB 功能。我也相信可以使用 CAB 的一些更高级功能来更好地实现这个向导,但它足以满足生产环境的需求,并且可能有用。
背景
我在这方面努力要做的一件事是使这个向导易于实现,同时又忠实于 CAB 的基本原则。我不想为了使用向导而编写整个指导包。我将在代码讲解中讨论我这样做的原因。
代码
我使用的是 VS 2005、CAB 框架和 Patterns and Practices Guidance Packages。
我从一个基本的 CAB Shell 开始,它是由指导包愉快地生成的。美好的时光。
我添加了 3 个 CAB 模块和一个类库项目。前两个模块(巧妙地命名为 ModuleA
和 ModuleB
)各包含一个视图,以说明可以从不同模块中显示向导视图。第三个模块包含我们将用于托管向导视图的视图。
类库仅包含两个接口,一个用于任何您想放入向导的视图的 UserControl
,另一个用于同一个视图的 presenter。这些接口允许我们将任何视图(无论它在哪个模块中)以相同的方式处理,从而保持 CAB 项目的口号(任何模块都不会引用任何其他模块)。

接口
namespace WizardInterface
{
public interface IWizardView
{
IWizardPresenter Presenter { get; }
int Priority { get; set; }
}
}
public interface IWizardPresenter
{
bool Startup();
bool Shutdown();
}
WizardView1
中接口的实现
namespace CABWizard.ModuleA
{
public class WizardView1Presenter : Presenter<IWizardView1>, IWizardPresenter
{
/// <summary>
/// This method is a placeholder that will be called by
/// the view when it's been loaded <see cref="System.Winforms.Control.OnLoad"/>
/// </summary>
public override void OnViewReady()
{
base.OnViewReady();
}
/// <summary>
/// Close the view
/// </summary>
public void OnCloseView()
{
base.CloseView();
}
#region IWizardPresenter Members
public bool Startup()
{
return true;
}
public bool Shutdown()
{
return true;
}
#endregion
}
}
namespace CABWizard.ModuleA
{
[SmartPart]
public partial class WizardView1 : UserControl, IWizardView1, IWizardView
{
public WizardView1()
{
InitializeComponent();
}
/// <summary>
/// Sets the presenter. The dependency injection system will automatically
/// create a new presenter for you.
/// </summary>
[CreateNew]
public WizardView1Presenter Presenter
{
set
{
_presenter = value;
_presenter.View = this;
}
}
protected override void OnLoad(EventArgs e)
{
_presenter.OnViewReady();
}
#region IWizardView Members
IWizardPresenter IWizardView.Presenter
{
get { return _presenter; }
}
private int priorityField;
public int Priority
{
get { return priorityField; }
set { priorityField = value; }
}
#endregion
}
}
您会注意到我必须将 Presenter 暴露为视图上的一个 public
属性。我真的不想这样做,而是想只与 presenters 通信,并让 Presenters 控制 Views。不幸的是,实例化 presenter 的 Views 的依赖注入仅在 View 添加到 WorkItem
时执行。不幸的是,如果我传递 Presenters 而不是 Views,我显然需要它们。手动创建 Presenter 也很麻烦,因为您必须手动设置 WorkItem
和 presenter 上的其他基本属性。这只是件很痛苦的事。
在获取要显示的视图列表方面,我们通过抛出事件来处理。一个事件,所有模块都必须订阅该事件,询问视图列表,然后通过更多事件将已实例化的视图传回 WizardModule
。
WizardModule
中的 RequestForView
事件,以及 ModuleA
中的订阅
[EventPublication(EventTopicNames.RequestForViews, PublicationScope.Global)]
public event EventHandler<EventArgs<List<string>>> RequestForViews;
protected virtual void OnRequestForViews(EventArgs<List<string>> eventArgs)
{
if (RequestForViews != null)
{
RequestForViews(this, eventArgs);
}
}
[EventSubscription(EventTopicNames.RequestForViews, ThreadOption.UserInterface)]
public void OnRequestForViews(object sender, EventArgs<List<string>> eventArgs)
{
List<IWizardView> views = new List<IWizardView>();
foreach (string s in eventArgs.Data)
{
switch (s)
{
case "WizardView1":
{
WizardView1 view = new WizardView1();
view.Priority = eventArgs.Data.IndexOf(s) + 1;
views.Add(view);
break;
}
case "WizardView3":
{
WizardView1 view = new WizardView1();
view.Priority = eventArgs.Data.IndexOf(s) + 1;
views.Add(view);
break;
}
}
}
OnReturnRequestedViews(views);
}
现在 ModuleA
使用的事件将视图列表传回 WizardModule
[EventPublication(EventTopicNames.ReturnRequestedViews, PublicationScope.Global)]
public event EventHandler<EventArgs<List<IWizardView>>> ReturnRequestedViews;
protected virtual void OnReturnRequestedViews(List<IWizardView> eventArgs)
{
if (ReturnRequestedViews != null)
{
ReturnRequestedViews(this, new EventArgs<List<IWizardView>>(eventArgs));
}
}
[EventSubscription(EventTopicNames.ReturnRequestedViews, ThreadOption.UserInterface)]
public void OnReturnRequestedViews(object sender, EventArgs<List<IWizardView>> eventArgs)
{
viewList.AddRange(eventArgs.Data);
//All the views that we asked for, have arrived.
//If they never arrive (i.e., a module didn't load) at least nothing *breaks*.
if (viewList.Count == viewNames.Count)
{
viewList.Sort(delegate(IWizardView i, IWizardView j)
{
return i.Priority.CompareTo(j.Priority);
});
viewList.Add((IWizardView)new EndOfWizard());
ShowWizardHost();
}
}
private void ShowWizardHost()
{
WizardHost wizardHost = new WizardHost(viewList);
WorkItem.SmartParts.Add(wizardHost, ViewNames.WizardHost);
WorkItem.Workspaces[WorkspaceNames.RightWorkspace].Show(wizardHost);
}
请注意,我在这里采取了捷径,其余的留给您。视图名称列表(或您决定用于获取视图的任何其他方法)是硬编码的,您可以在此处使用服务、向导生成器或您想要的任何其他机制。
我们在 WizardHost
视图上创建一个新的 Workspace
,然后将 Workspace
添加到 WorkItem
。然后我们在 WizardHost
的 DeckWorkspace
中显示第一个向导视图。有简单的逻辑处理“下一步”和“上一步”按钮,然后一切就绪。
Presenter 上最重要的事情是您的 Startup
方法。如果它返回 false
,向导主机将跳过此视图并继续前进。您仍然可以在返回布尔值之前在此处执行代码。因此,如果您需要一个“不可见”的视图作为保存点,您可以拥有它。Startup 方法也用于设置视图中的数据,调用数据库,或者视图需要的任何内容。
您还会注意到 WizardView1
出现了两次。这只是为了说明向导中可以有多个相同类型的视图,如果您的视图执行多个角色,则很有用。要检查向导的行为,请将 wizardview1
上的 return true;
值设置为 return false;
。观察向导如何毫无问题地继续进行,跳过 WizardView1
的两个实例。
关注点
这是一个*非常*基础的向导。您可以添加许多其他功能,例如
- 添加属性以调整向导的行为,以确保在视图满足所有标准之前,您无法继续前进(例如,验证)。
- 通过一些技巧,您可以创建一个“动态”向导,它允许“实时”更改视图列表。
- 您可以向向导主机添加一个进度条,显示用户当前正在查看哪个视图,以及还剩下多少步。
- 您当然可以让它变得更漂亮:)
历史
- 2009年3月13日:初始发布