演示选择器
选择适合特定领域对象的屏幕
2004 年 8 月 31 日
这是我在 2000 年代中期进行的 进一步的企业应用程序架构开发 写作的一部分。遗憾的是,此后太多其他事情吸引了我的注意力,所以我没有时间进一步研究它们,而且在可预见的未来我也没有看到太多时间。因此,这些材料非常草稿形式,我不会进行任何更正或更新,直到我能找到时间再次处理它。
当屏幕之间的导航由演示文稿引导时,演示文稿可以将要显示的屏幕序列直接编码。这样,演示文稿始终知道在发生适当事件时需要打开哪个屏幕。但是,有时应该显示的屏幕取决于域中的信息。演示文稿不知道应该显示哪个屏幕,而是知道它应该在某个窗口中显示一个域对象。域对象不能选择屏幕,因为这将违反 分离的演示文稿
一个 演示选择器 解决应该为特定域对象使用哪个屏幕。客户端知道它应该打开一个新屏幕来显示特定域对象。客户端询问 演示选择器 应该为该域对象使用哪个屏幕,并打开返回的窗口。
工作原理
在最简单的形式中,您可以将 演示选择器 视为一个字典查找,它将域对象的类型与屏幕的类型索引在一起。
Order => OrderWindow Customer => CustomerWindow PriorityCustomer => PriorityCustomerWindow
使用这种简单模式,查找适当的屏幕类似于 aPresentationChooser[domainObject.type]
。
尽管这个简单想法是 演示选择器 的本质,但有一些常见区域可能会变得更加复杂。其中大多数源于这样一个事实,即尽管大多数查找完全基于域对象的类型,但并非所有查找都是如此。因此,通常不值得将 演示选择器 的类型查找性质暴露给它的客户端,因此,与其要求客户端传入域对象的类型,不如传入对象本身。这遵循一个一般原则,即对象永远不应该要求客户端做它自己可以合理地做的事情。它还允许 演示选择器 在需要时提供更复杂的查找规则。
演示选择器 通常是一个 服务。在初始化期间,一些配置模块将使用有关屏幕和域对象如何映射在一起的详细信息来初始化 演示选择器。然后在正常执行期间,屏幕使用 演示选择器 来查找屏幕。
何时使用它
当您必须使用不同的屏幕类(通常是窗口,但可能是表单中的嵌入式面板)来响应相同的导航流程时,您需要 演示选择器。一个简单的例子,正如我在下面使用的那样,是您在一个列表中具有不同类型的对象,并且您需要为每种类型使用不同的屏幕。
通常,处理此类情况的最佳方法是使用相同类型的屏幕,但在该屏幕上使用隐藏和禁用的控件来阻止对不适当数据的访问。如果类型之间的差异很小,并且您可以在没有令人不快的界面情况下使用类似的屏幕,那么这可以很好地工作。当屏幕加载时,您会询问域对象以决定要启用和显示哪些控件。
但是,使用相同的可变屏幕会为您提供更少的自定义显示底层域对象的选项。有时,相似之处最终会让用户感到困惑,而不是帮助他们。在这些情况下,最好使用完全不同的屏幕,而 演示选择器 很有用,可以确定使用哪一个。
虽然我在这里集中讨论根据域数据改变屏幕,但变化还可以包括整体自定义因素,例如用户、用户的关联、同一应用程序的多个提供者或交互状态的不同方面。所有这些自定义都可能导致动态地使用不同的屏幕类,因此意味着在选择要使用的实际屏幕类时存在间接性。在这些情况下, 演示选择器 是提供这种间接性的好方法。
演示选择器 和 应用程序控制器 都解耦了触发导航的屏幕类的选择。 演示选择器 最适合要显示的域对象是主要变化的情况, 应用程序控制器 更适合应用程序状态是主要变化的情况。当两者都发生变化时,将模式组合在一起是有意义的。
示例:简单查找(C#)
演示选择器 的最简单示例是具有基于域对象类型进行简单查找功能的示例。考虑一个显示有关音乐录音信息的应用程序。录音显示在一个列表中,任何一个都可以编辑。但是,底层录音需要根据它们是古典录音还是流行录音进行不同的编辑。
图 1:用于选择录音的窗口。
为了对此做出反应,我有一个用于单击编辑按钮的事件处理程序
private void btnEdit_Click(object sender, EventArgs e) { edit(Recordings[lstTitles.SelectedIndex]); } private void edit(IRecording recording) { Chooser.ShowDialog(recording); }
此模式的关键是 edit 方法,它会询问 演示选择器 根据所选录音是古典录音还是流行录音来显示用于编辑所选录音的对话框。
在这种情况下, 演示选择器 实际上只是一个字典查找。在初始化期间,我们使用域类型和要使用的相应窗口的详细信息加载 演示选择器。
class PresentationChooser...
protected IDictionary presenters = new Hashtable(); public virtual void RegisterPresenter(Type domainType, Type presentation) { presenters[domainType] = presentation; }
此初始化只是简单地将域和演示类型存储在字典中。查找过程稍微复杂一些。如果我稍后添加古典录音的子类型,我希望它由古典录音编辑,除非我为该子类型注册了屏幕。
class PresentationChooser...
public RecordingForm ShowDialog (Object model) { Object[] args = {model}; RecordingForm dialog = (RecordingForm) Activator.CreateInstance(this[model], args); dialog.ShowDialog(); return dialog; } public virtual Type this [Object obj] { get { Type result = lookupPresenter(obj.GetType()); if (result == null) MessageBox.Show("Unable to show form", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return result; } } private Type lookupPresenter(Type arg) { Type result = (Type)presenters[arg]; return (null == result) ? lookupPresenter(arg.BaseType) : result; }
我写了这个代码来显示一个错误消息对话框,如果它找不到已注册的屏幕。我可以通过在注册屏幕时在层次结构的顶部提供屏幕来避免需要显示此对话框。
presentationChooser.RegisterPresenter(typeof(ClassicalRecording), typeof(FrmClassicalRecording)); presentationChooser.RegisterPresenter(typeof(PopularRecording), typeof (FrmPopularRecording)); presentationChooser.RegisterPresenter(typeof(Object), typeof (FrmNullPresentation));
示例:条件(C#)
在这里,我使用上面的简单示例,并通过允许一些更动态的选择器行为来使其稍微复杂一些。在这种情况下,我希望大多数古典录音由常规的古典显示表单显示,但对于莫扎特创作的任何作品,都使用特殊的显示表单。(我不确定为什么我要这样做,但我不太擅长在星期二想出令人信服的例子。)
演示选择器 的大部分界面与更简单的版本相同。显示录音对话框的代码仍然只是 Chooser.showDialog(aRecording)
。但是, 演示选择器 的实现稍微复杂一些。
我仍然使用按类型索引的字典来存储信息。但是这次,我想为每个域对象有多个屏幕。我可以使用一个类来捕获每个选择。
class DynamicPresentationChooser…
Type _registeredType; Type _presentation; ConditionDelegate _condition; public delegate bool ConditionDelegate(Object recording); public PresentationChoice(Type registeredType, Type presentation, ConditionDelegate condition) { this._registeredType = registeredType; this._presentation = presentation; this._condition = condition; }
在这里,我使用 C# 委托来允许客户端传入一个布尔函数,以对域对象评估条件。新的查找功能按顺序检查列表中的所有选择,返回第一个其条件委托对给定域对象求值为真的选择。
class PresentationChoice...
public override Type this[Object obj] { get { IList list= presenterList(obj.GetType()); return (null == list) ? typeof(FrmNullPresentation): chooseFromList(list, obj); } } private IList presenterList(Type type) { IList result = (IList) presenters[type]; if (null != result) return result; else if (typeof (object) != type) return presenterList(type.BaseType); else return new ArrayList(); } private static Type chooseFromList(IList list, object domainObject) { foreach (PresentationChoice choice in list) if (choice.Matches(domainObject)) return choice.Presentation; return typeof(FrmNullPresentation); }
如果没有任何匹配项,我会返回一个 空对象。
为了注册屏幕,我决定我想维护与上面简单情况兼容的界面。为此,我允许仅使用域和屏幕类型进行注册,以与之前使用几个便利方法的方式一样工作。
class DynamicPresentationChooser...
public override void RegisterPresenter(Type domainType, Type presentation) { presenters[domainType] = new ArrayList(); presenterList(domainType).Add (new PresentationChoice (domainType, presentation)); }
class PresentationChoice...
public PresentationChoice(Type domainType, Type presentation) : this (domainType, presentation, null){ _condition = new ConditionDelegate(TrueConditionDelegate); } public static bool TrueConditionDelegate(Object ignored) {return true;}
这样,我可以在任何情况下使用动态选择器,否则我会使用简单选择器。
然后,我使用额外的注册方法来添加动态选项。我已经设置了这个,所以只有在我已经存在默认选择的情况下才能这样做。这使得配置界面比我想要的稍微笨拙一些,但这样任何问题都会在配置中显示出来,因此结果会快速失败。
class DynamicPresentationChooser...
public void RegisterAdditionalPresenter(Type domainType, Type presentation, PresentationChoice.ConditionDelegate condition) { Debug.Assert( null != presenterList(domainType), String.Format("Must register default choice for {0} first", domainType)); presenterList(domainType).Insert(0, new PresentationChoice(domainType, presentation, condition)); }
然后,我可以像这样注册类。
presentationChooser.RegisterPresenter(typeof(ClassicalRecording), typeof(FrmClassicalRecording)); presentationChooser.RegisterPresenter(typeof(PopularRecording), typeof (FrmPopularRecording)); chooser.RegisterAdditionalPresenter( typeof(ClassicalRecording), typeof(FrmMozartRecording), new PresentationChoice.ConditionDelegate(MozartCondition));