文章目录

  • 一些Windows窗体基础
  • 服务和容器
  • 让我们构建一个窗体设计器
  • 实施服务
  • 设计器Host
  • 设计器事务
  • 接口
  • 设计器交互显示上下文
  • ITypeDescriptorFilterService
  • 综合起来
  • 调试项目
  • 结论

一些Windows窗体基础

在我开始这个项目之前,有几个基本概念很重要。让我们从设计器的定义开始。设计器提供设计模式 UI 和组件的行为。例如,当您在窗体上放置按钮时,按钮的设计器是确定按钮的外观和行为的实体。设计时环境提供窗体设计器和属性编辑器,允许您操作组件和构建用户界面。设计时环境还提供可用于与设计时间支持进行交互、自定义和扩展的服务。

窗体设计器为开发人员提供设计时服务和设计表单的设施。设计器主机与设计时环境一起管理设计器状态、活动(如事务)和组件。此外,还有几个与组件本身相关的概念非常重要。例如,组件是一次性的,可以由容器管理,并提供Site属性。它通过实现IComponent获得这些特性,如下所示:

public interface System.ComponentModel.IComponent : IDisposable
{ ISite Site { get; set; } public event EventHandler Disposed;
}

IComponent接口是设计时间环境和要托管在设计表面上的元素(如Visual Studio窗体设计器)之间的基本协定。例如,可以在Windows窗体设计器上托管按钮,因为它实现了IComponent

.NET框架实现两种类型的组件:可视和非可视组件。可视组件是用户界面元素(如控件),非可视组件是没有用户界面的组件,例如创建SQL Server ™连接的组件。当您将组件拖放到设计图面上时,Visual Studio .NET 窗体设计器可区分视觉组件和非可视组件。图 1 显示了这种区别的示例。

容器包含组件,并允许包含的组件相互访问。当容器管理组件时,容器负责在释放容器时释放组件,这是个好主意,因为组件可能使用非托管资源,而垃圾回收器不会自动释放这些资源。容器实现 IContainer,它只不过是几个方法,允许您从容器中添加和删除组件:

public interface IContainer : IDisposable
{ ComponentCollection Components { get; } void Add(IComponent component);void Add(IComponent component, string name); void Remove(IComponent component);
}

不要让界面的简单性愚弄你。容器的概念在设计时至关重要,在其他情况下也很有用。例如,您肯定已经编写的业务逻辑实例化了几个一次性组件。这通常采用以下形式:

using(MyComponent a = new MyComponent())
{// a.do();
}
using(MyComponent b = new MyComponent())
{ // b.do();
}
using(MyComponent c = new MyComponent())
{ // c.do();}

使用容器对象,这些行将简化为以下内容:

using(Container cont = new Container())
{ MyComponent a = new MyComponent(cont); MyComponent b = new MyComponent(cont); MyComponent c = new MyComponent(cont); // a.do(); // b.do(); // c.do();
}

容器比自动处理其组件更重要。.NET框架定义所谓的站点,它与容器和组件相关。图2显示了这三者之间的关系。如您所见,组件只由一个容器管理,每个组件只有一个站点。生成窗体设计器时,同一组件不能显示在多个设计图面上。但是,多个组件可以与同一容器关联。

组件的生命周期可以由其容器控制。作为生存期管理的回报,组件获得对容器提供的服务的访问权限。此关系类似于位于COM+容器内的 COM+组件。通过允许COM+容器管理它,COM+组件可以参与事务并使用COM+容器提供的其他服务。在设计时上下文中,组件与其容器之间的关系通过站点建立。将组件放在窗体上时,设计器主机为组件及其容器创建一个站点实例。建立此关系后,组件已被"站点化",并使用其ISite属性访问其容器提供的服务。

服务和容器

当组件允许容器拥有它的所有权时,该组件将访问该容器提供的服务。在此上下文中,服务可被视为具有已知接口的函数,可以从服务提供商处获取,存储在服务容器中,并且可按其类型进行地址处理。

服务提供商实现IServiceProvider,如下所示:

public interface IServiceProvider
{ object GetService(Type serviceType);
}

客户端通过向服务提供商的GetService方法提供所需的服务类型来获取服务。服务容器充当服务的存储库并实现IServiceContainer,从而提供了一种添加和删除服务的方法。以下代码显示了IServiceContainer的定义。请注意,服务定义仅包含添加和删除服务的方法。

public interface IServiceContainer : IServiceProvider
{ void AddService(Type serviceType,ServiceCreatorCallback callback); void AddService(Type serviceType,ServiceCreatorCallback callback, bool promote); void AddService(Type serviceType, object serviceInstance); void AddService(Type serviceType, object serviceInstance, bool promote); void RemoveService(Type serviceType); void RemoveService(Type serviceType, bool promote);
}

由于服务容器可以存储和检索服务,因此它们也被视为服务提供者,因此实现IServiceProvider。服务、服务提供商和服务容器的组合构成了一个简单的设计模式,具有许多优点。例如,模式:

  • 在客户端组件及其使用的服务之间创建松散耦合。
  • 创建一个简单的服务存储库和发现机制,允许应用程序(或部分应用程序)很好地扩展。您可以使用所需的部分构建应用程序,然后稍后添加其他服务,而无需对应用程序或模块进行任何剧烈更改。
  • 提供实现延迟加载服务所需的工具。AddService方法在首次查询时重载以创建服务。
  • 可用作静态类的替代。
  • 促进基于合同的编程。
  • 可用于实现工厂服务。
  • 可用于实现可插拔的体系结构。您可以使用此简单模式加载插件,并为插件提供服务(如日志记录和配置)。

设计时基础结构非常广泛地使用此模式,因此彻底理解它非常重要。

让我们构建一个窗体设计器

现在您已经了解了设计时间环境背后的基本概念,我将通过检查表单设计器的体系结构来构建这些概念(参见图 3)。

体系结构的核心位于组件。所有其他实体直接或间接地使用组件。窗体设计器是连接其他实体的粘合剂。窗体设计器使用设计器主机访问设计时基础结构。设计器主机使用设计时服务,并提供自己的一些服务。服务可以而且经常使用其他服务。

.NET 框架不会公开Visual Studio .NET中的窗体设计器,因为该实现是特定于应用程序的。即使实际接口未公开,设计时框架也存在。您所有需要做的就是提供特定于表单设计器的实现,然后将版本提交到要使用的设计时间环境。

我的示例窗体设计器如图 4 所示。与每个窗体设计器一样,它有一个工具箱供用户选择工具或控件,一个用于生成窗体的设计图和一个用于操作组件属性的属性网格。

首先,我将构建工具箱。但是,在这样做之前,我需要决定如何向用户展示工具。Visual Studio .NET具有一个导航栏,其中包含多个组,每个组都包含工具。若要生成工具箱,必须执行以下操作:

  1. 创建向用户显示工具的用户界面
  2. 实现IToolbox服务
  3. IToolbox服务实现插入设计时间环境
  4. 处理事件,如工具选择和拖放

对于任何实际应用程序,构建工具箱用户界面可能非常耗时。您必须做出的第一个设计决策是如何发现和加载工具,并且有几种可行的方法。使用第一种方法,您可以硬编码要显示的工具。不建议这样做,除非您的应用程序非常简单,并且将来需要很少的维护。

第二种方法涉及从配置文件中读取工具。例如,工具可以定义如下:

<Toolbox> <ToolboxItems><ToolboxItem DisplayName="Label" Image="ResourceAssembly,Resources.LabelImage.gif"/><ToolboxItem DisplayName="Button" Image="ResourceAssembly,Resources.ButtonImage.gif"/> <ToolboxItem DisplayName="Textbox" Image="ResourceAssembly,Resources.TextboxImage.gif"/> </ToolboxItems>
</Toolbox>

此方法的优点是,您可以添加或减去工具,并且不必重新编译代码来更改工具箱中显示的工具。此外,实现相当简单。实现节处理程序以读取工具箱部分并返回工具箱项列表。

第三种方法是为每个工具创建一个类,并用封装显示名称、组和位图等内容的属性来修饰该类。启动时,应用程序加载一组程序集(从配置文件中指定的已知位置或类似内容),然后搜索具有特定修饰的类型(如ToolboxAttribute)。具有此修饰的类型将加载到工具箱中。此方法可能是最灵活的,允许通过反射发现伟大的工具,但它也需要更多的工作。在我的示例应用程序中,我使用第二种方法。

下一个重要步骤是获取工具箱图像。您可以花费数天时间尝试创建自己的工具箱映像,但以某种方式访问Visual Studio .NET工具箱中的工具箱图像会非常方便。幸运的是,有一种方法可以做到这一点。在内部,使用第三种方法的变体加载Visual Studio .NET工具箱。这意味着组件和控件使用属性(ToolboxBitmapAttribute)进行修饰,该属性定义在什么地方获取组件或控件的图像。

在示例应用程序中,工具箱内容(组和项)在应用程序配置文件中定义。要加载工具箱,自定义节处理程序将读取工具箱部分并返回绑定类。然后将绑定类传递给表示工具箱的TreeView控件的LoadToolbox方法,如图 5 所示。

/// <summary>
/// used to load the toolbox
/// </summary>
/// <param name="tools"></param>
public void LoadToolbox(FdToolbox tools)
{// clear out existing nodes and the imageList associated with the treetoolboxView.Nodes.Clear();treeViewImgList.Dispose();treeViewImgList = new ImageList(components);// we have two images that we always use for category nodes// and the select tool node (pointer node).// add these in nowtreeViewImgList.Images.Add(requiredImgList.Images[0]);treeViewImgList.Images.Add(requiredImgList.Images[1]);// assign imageList to the treeViewtoolboxView.ImageList = treeViewImgList;// if we have categories...if (tools?.FdToolboxCategories == null || tools.FdToolboxCategories.Length <= 0) return;foreach (Category category in tools.FdToolboxCategories) LoadCategory(category);
}/// <summary>
/// loads a group of toolbox items into the under the given category
/// </summary>
/// <param name="category"></param>
private void LoadCategory(Category category)
{// if we have items in the category...if (category?.FdToolboxItem == null || category.FdToolboxItem.Length <= 0) return;// create a node for the categoryTreeNode catNode = new TreeNode(category.DisplayName) { ImageIndex = 0, SelectedImageIndex = 0 };// add this category to the treetoolboxView.Nodes.Add(catNode);// every category gets the selection tool nodeAddSelectionNode(catNode);foreach (FdToolboxItem item in category.FdToolboxItem) LoadItem(item, catNode);
}/// <summary>
/// loads an item into the tree
/// </summary>
/// <param name="item"></param>
/// <param name="cat"></param>
private void LoadItem(FdToolboxItem item, TreeNode cat)
{if (item?.Type == null || cat == null) return;// load the typestring[] assemblyClass = item.Type.Split(',');Type toolboxItemType = GetTypeFromLoadedAssembly(assemblyClass[0], assemblyClass[1]);//ToolboxItem toolItem = new ToolboxItem(toolboxItemType);// get the image for the itemImage img = GetItemImage(toolboxItemType);// create the node for itTreeNode nd = new TreeNode(toolItem.DisplayName);// add the item's bitmap to the image listif (img != null){// add it to the image listtreeViewImgList.Images.Add(img);// set the nodes image indexnd.ImageIndex = treeViewImgList.Images.Count - 1;// we have to set both the node's ImageIndex and// SelectedImageIndex or we get some wierd behavior// when we select nodes (the selected nodes image changes)...nd.SelectedImageIndex = treeViewImgList.Images.Count - 1;}nd.Tag = toolItem;// add this node to the category nodecat.Nodes.Add(nd);
}/// <summary>
/// finds the image associated with the type
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private Image GetItemImage(Type type)
{// get the AttributeCollection for the given type and// find the ToolboxBitmap attribute AttributeCollection attrCol = TypeDescriptor.GetAttributes(type, true);ToolboxBitmapAttribute toolboxBitmapAttr = (ToolboxBitmapAttribute)attrCol[typeof(ToolboxBitmapAttribute)];return toolboxBitmapAttr?.GetImage(type);
}

LoadItem 方法为给定类型创建一个工具箱Item实例,然后调用 GetItemImage 获取与该类型关联的图像。该方法获取类型的属性集合,以查找工具箱位图属性。如果它找到该属性,它将返回图像,以便它可以与新创建的ToolboxItem关联。请注意,该方法使用TypeDescriptor类,这是 System.ComponentModel 命名空间中的一个实用程序类,用于获取给定类型的属性和事件信息。

现在您已了解如何构建工具箱用户界面,下一步是实现 IToolbox 服务。由于此接口直接绑定到工具箱,因此只需在 TreeView 派生类中实现此接口就很方便了。大多数实现都很简单,但您确实需要特别注意如何处理拖放操作以及如何序列化工具箱项(请参阅本文代码下载中的工具箱服务实现中的 toolboxView_MouseDown方法,可从 MSDN® 杂志网站获得)。该过程的最后一步是将服务实现连接到设计时间环境,在讨论如何实现设计器主机后,我将演示如何执行该设计时间环境。

实施服务

窗体设计器基础结构构建在服务之上。需要实现一组服务,如果实现窗体设计器,则有一些服务只是增强窗体设计器的功能。这是我前面谈到的服务模式以及表单设计器的一个重要方面。您可以首先实现基集,然后稍后添加其他服务。

设计器主机是进入设计时环境的挂钩。设计时环境使用主机服务在用户从工具箱中拖放组件、管理设计器事务、在用户操作组件时查找服务等时创建新组件。主机服务定义IDesignerHost定义方法和事件。在主机实现中,您为主机服务提供实现以及其他几个服务。这些应该包括IContainerIComponentChangeServiceIExtenderProviderServiceITypeDescriptionFilterServiceIDesignerEventService

设计器Host

设计者Host是窗体设计器的核心。当调用主机的构造函数时,Host使用父服务提供者(IServiceProvider) 构造其服务容器。以这种方式进行链式提供商,以达到滴流效应,这种情况很常见。创建服务容器后,主机会向提供程序添加自己的服务,如图 6 所示。

public DesignerHostImpl(IServiceProvider parentProvider)
{// append to the parentProvider..._serviceContainer = new ServiceContainer(parentProvider);// site name to ISite mapping_sites = new Hashtable(CaseInsensitiveHashCodeProvider.Default, CaseInsensitiveComparer.Default);// component to designer mapping_designers = new Hashtable();// list of extender providers_extenderProviders = new ArrayList();// create transaction stack_transactions = new Stack();// services_serviceContainer.AddService(typeof(IDesignerHost), this);_serviceContainer.AddService(typeof(IContainer), this);_serviceContainer.AddService(typeof(IComponentChangeService), this);_serviceContainer.AddService(typeof(IExtenderProviderService), this);_serviceContainer.AddService(typeof(IDesignerEventService), this);_serviceContainer.AddService(typeof(INameCreationService), new NameCreationServiceImpl(this));_serviceContainer.AddService(typeof(ISelectionService), new SelectionServiceImpl(this));_serviceContainer.AddService(typeof(IMenuCommandService), new MenuCommandServiceImpl(this));_serviceContainer.AddService(typeof(ITypeDescriptorFilterService), new TypeDescriptorFilterServiceImpl(this));
}

当组件被放到设计图面时,需要将组件添加到主机的容器中。添加新组件是一个相当复杂的操作,因为您必须执行多个检查并关闭一些事件(参见图 7)。

public void Add(IComponent component, string name)
{// we have to have a componentif (component == null)throw new ArgumentException("component");// if we don't have a name, create oneif (name == null || name.Trim().Length == 0){// we need the naming serviceif (!(GetService(typeof(INameCreationService)) is INameCreationService nameCreationService))throw new Exception("Failed to get INameCreationService.");name = nameCreationService.CreateName(this, component.GetType());}// if we own the component and the name has changed// we just rename the componentif (component.Site != null && component.Site.Container == this &&name != null && string.Compare(name, component.Site.Name, true) != 0){// name validation and component changing/changed events// are fired in the Site.Name property so we don't have // to do it here...component.Site.Name = name;// bail outreturn;}// create a site for the componentISite site = new SiteImpl(component, name, this);// create component-site associationcomponent.Site = site;// the container-component association was established when // we created the site through site.host.// we need to fire adding/added events. create a component event args // for the component we are adding.ComponentEventArgs evtArgs = new ComponentEventArgs(component);// fire off adding eventif (ComponentAdding != null){try{ComponentAdding(this, evtArgs);}catch { }}// if this is the root componentIDesigner designer = null;if (_rootComponent == null){// set the root component_rootComponent = component;// create the root designerdesigner = (IRootDesigner)TypeDescriptor.CreateDesigner(component, typeof(IRootDesigner));}else{designer = TypeDescriptor.CreateDesigner(component, typeof(IDesigner));}if (designer != null){// add the designer to the list_designers.Add(component, designer);// initialize the designerdesigner.Initialize(component);}// add to container component list_sites.Add(site.Name, site);// now fire off added eventif (ComponentAdded == null) return;try{ComponentAdded(this, evtArgs);}catch { }
}

如果您忽略检查和事件,可以总结添加算法,如下所示。首先,为该类型创建一个新的IComponent,为组件创建一个新的ISite。这将建立站点到组件的关联。请注意,站点的构造函数接受设计器主机实例。站点构造函数采用设计器主机和组件,以便它可以建立图 2 中所示的组件-容器关系。然后创建、初始化组件设计器,并添加到组件到设计器字典中。最后,新组件将添加到设计器主机容器中。

删除组件需要一些清理。同样,忽略简单的检查和验证,删除操作相当于删除设计器,释放设计器,删除组件的站点,然后释放组件。

设计器事务

设计器事务的概念类似于数据库事务,因为它们都对一系列操作进行分组,以便将组视为工作单元并启用提交/中止机制。设计器事务在整个设计时基础结构中使用,以支持取消操作,并启用视图延迟其显示的更新,直到整个事务完成。设计器主机提供通过IDesignerHost接口管理设计器事务的工具。管理事务不是非常困难(请参阅DesignerTransactionImpl.cs应用程序中的一个示例)。

设计器事务,代表事务中的单个操作。当要求主机创建事务时,它会创建一个Designer事务Impl的实例来管理单个更改。当Designer事务Impl的实例管理每个更改时,主机跟踪事务。如果不实现事务管理,则在使用窗体设计器时,会得到一些有趣的异常。

接口

正如我说过的,组件被放入容器中,以进行终身管理,并为他们提供服务。设计器主机接口IDesignerHost定义了创建和删除组件的方法,因此,如果主机提供此服务,您就不应该感到惊讶。同样,容器服务定义添加和删除组件的方法,这些方法与IDesignerHostCreate 组件和销毁组件方法重叠。因此,大部分繁重的工作都是在容器的添加和删除方法中完成的,创建和销毁方法只需将调用转发到这些方法。

IComponentChangeService定义组件更改、添加、删除和重命名事件。它还定义组件更改和更改事件的方法,当组件更改或已更改时(例如,当属性更改时),设计时环境会调用这些方法。此服务由设计器主机提供,因为组件通过主机创建和销毁。除了创建和销毁组件外,主机还通过创建方法处理组件重命名操作。重命名逻辑很简单,然而很有趣:

// If I own the component and the name has changed, rename the component
if (component.Site != null && component.Site.Container == this && name != null && string.Compare(name,component.Site.Name,true) != 0)
{ // name validation and component changing/changed events are // fired in the Site.Name property so I don't have // to do it here... component.Site.Name=name; return;
}

此接口的实现非常简单,您可以将其余部分推迟到示例应用程序。

ISelectionService处理设计图面上的组件选择。当用户选择组件时,由具有所选组件的设计时间环境调用SetSelectedComponents方法。SetSelectedComponents的实现如图 8 所示。

public void SetSelectedComponents(ICollection components, SelectionTypes selectionType)
{// fire changing eventif (SelectionChanging != null){try{SelectionChanging(this, EventArgs.Empty);}catch{// ignored}}// don't allow an empty collectionif (components == null || components.Count == 0) {components = new ArrayList();}bool ctrlDown=false,shiftDown=false;// we need to know if shift or ctrl is down on clicks if ((selectionType & SelectionTypes.Primary) == SelectionTypes.Primary){ctrlDown = ((Control.ModifierKeys & Keys.Control) == Keys.Control);shiftDown = ((Control.ModifierKeys & Keys.Shift)   == Keys.Shift);}if (selectionType == SelectionTypes.Replace){// discard the hold list and go with this one_selectedComponents = new ArrayList(components);}else{if (!shiftDown && !ctrlDown && components.Count == 1 && !_selectedComponents.Contains(components)){_selectedComponents.Clear();}// something was either added to the selection// or removedIEnumerator ie = components.GetEnumerator();while(ie.MoveNext()){if (!(ie.Current is IComponent comp)) continue;if (ctrlDown || shiftDown){if (_selectedComponents.Contains(comp)){_selectedComponents.Remove(comp);}else{// put it back into the front because it was// the last one selected_selectedComponents.Insert(0,comp);}}else{if (!_selectedComponents.Contains(comp)){_selectedComponents.Add(comp);}else{_selectedComponents.Remove(comp);_selectedComponents.Insert(0,comp);}}}}// fire changed eventif (SelectionChanged == null) return;try{SelectionChanged(this, EventArgs.Empty);}catch{// ignored}
}

ISelectionService跟踪设计器表面上的组件选择。其他服务(如IMenuCommandService)在需要获取有关所选组件的信息时使用此服务。为了提供此信息,服务维护一个内部列表,表示当前选定的组件。设计时环境调用SetSelectedComponents,当对组件的选择进行了更改时,它具有组件的集合。例如,如果用户选择一个组件,然后关闭 shift 键并选择另外三个组件,则对每次添加到选择列表的每个组件都调用该方法。每次调用该方法时,设计时环境会告诉我们哪些组件受到影响以及如何(通过选择类型枚举)。实现着眼于如何更改组件,以确定是否需要向内部选定列表中添加或删除组件。修改内部选择列表后,我将调用"选择更改"事件(SelectionServiceImpl.csselectionService_SelectionChanged方法更改),以便可以使用新选择更新属性网格。应用程序的主要窗体MainWindow订阅选择服务的选择更改事件,以便使用所选组件更新属性网格。

另请注意,选择服务定义主选择属性。主选择始终设置为选择的最后一个项目。当我谈论显示正确的设计器上下文菜单时,我将使用此属性讨论IMenuCommandService

选择服务是最难正确实现的服务之一,因为它具有一些使实现复杂化的宝贵功能。例如,在实际应用程序中,处理键盘事件(如 Ctrl+A)以及管理有关处理大型选择列表的问题是有意义的。

ISite 实现是更重要的实现之一,如图 9 所示。

/// <summary>
/// Summary description for SiteImpl.
/// </summary>
public class SiteImpl : ISite, IDictionaryService
{private readonly IComponent _component;private readonly DesignerHostImpl _host;private readonly DictionaryServiceImpl _dictionaryService;private string _name;public SiteImpl(IComponent comp, string name, DesignerHostImpl host){if (name == null || name.Trim().Length == 0)throw new ArgumentException("name");_component = comp ?? throw new ArgumentException("comp");_host = host ?? throw new ArgumentException("host");_name = name;// create a dictionary service for this site_dictionaryService = new DictionaryServiceImpl();}#region ISite Memberspublic IComponent Component => _component;public IContainer Container => _host.Container;public bool DesignMode => true;public string Name{get => _name;set{// null name is not validif (value == null)throw new ArgumentException("value");// if we have the same nameif (string.Compare(value, _name, false) == 0) return;// make sure we have a valid nameINameCreationService nameCreationService = (INameCreationService)_host.GetService(typeof(INameCreationService));if(nameCreationService==null)throw new Exception("Failed to service: INameCreationService");if (!nameCreationService.IsValidName(value)) return;DesignerHostImpl hostImpl = (DesignerHostImpl)_host;// get the current namestring oldName = _name;// set the new nameMemberDescriptor md = TypeDescriptor.CreateProperty(_component.GetType(), "Name", typeof(string), new Attribute[] {});// fire changing eventhostImpl.OnComponentChanging(_component, md);// set the value_name = value;// we also have to fire the rename event_host.OnComponentRename(_component,oldName,_name);// fire changed eventhostImpl.OnComponentChanged(_component, md, oldName, _name);}}#endregion#region IServiceProvider Memberspublic object GetService(Type service){return service == typeof(IDictionaryService) ? this : _host.GetService(service);// forward request to the host}#endregion#region IDictionaryService Implementationpublic object GetKey(object value){return _dictionaryService.GetKey(value);}public object GetValue(object key){return _dictionaryService.GetValue(key);}public void SetValue(object key, object value){_dictionaryService.SetValue(key,value);}#endregion
}

您会注意到 SiteImpl 也实现了IDictionaryService,这有点不寻常,因为我实现的所有其他服务都与设计器主机绑定。事实证明,设计时环境需要您为每个站点组件实现IDictionaryService。设计时环境使用每个站点上的IDictionaryService来维护在整个设计器框架中使用的数据表。关于站点实现,需要注意的另一件事是,由于ISite扩展了IServiceProvider,因此该类提供了GetService的实现。设计器框架在站点上查找服务实现时调用此方法。如果服务请求是IDictionaryService,则实现只是返回自身,即 SiteImpl。对于所有其他服务,请求将转发到站点的容器(例如主机)。

每个组件必须具有唯一的名称。当您将组件从工具箱拖放到设计图面时,设计时环境使用 INameCreationService的实现来生成每个组件的名称。组件的名称是选择组件时在属性窗口中显示的 Name属性。INameCreationService接口的定义如下所示:

public interface INameCreationService
{ string CreateName(IContainer container, Type dataType); bool IsValidName(string name); void ValidateName(string name);
}

在示例应用程序中,CreateName实现使用容器和dataType来计算新名称。简而言之,该方法计算其类型等效于dataType的组件数,然后使用与dataType一起计数来显示唯一的名称。

迄今讨论的服务都直接或间接地处理了组件。另一方面,菜单命令服务特定于设计人员。它负责跟踪菜单命令和设计器谓词(操作),并在用户选择特定设计器时显示正确的上下文菜单。

菜单命令服务处理添加、删除、查找和执行菜单命令的任务。此外,它还定义了跟踪设计器谓词和为支持这些谓词的设计者显示设计器上下文菜单的方法。此实现的核心在于显示正确的上下文菜单。因此,我将推迟将剩下的小实现提交到示例应用程序,而是侧重于如何显示上下文菜单。

设计器交互显示上下文

设计器动词有两种类型:全局动词和本地类动词。所有设计器都存在全局动词,并且本地动词特定于每个设计器。右键单击设计图面上的选项卡控件时,可以看到本地动词的示例(参见图 10)。

右键单击选项卡控件将添加本地谓词,允许您在控件上添加和删除选项卡。当您右键单击设计图图上的任意位置时,可以在 Visual Studio 窗体设计器中看到全局动词的示例。无论单击的对象位于什么位置和位置,您始终会看到两个菜单项:查看代码和属性。每个设计器都有一个 Verbs 属性,其中包含表示特定于该设计器的功能的动词。例如,对于选项卡控件设计器,谓词集合包含两个成员:添加选项卡和删除选项卡。

当用户右键单击设计图面上的选项卡控件时,设计时环境将调用IMenuCommandService上的 ShowContextMenu方法(参见图 11)。

public void ShowContextMenu(System.ComponentModel.Design.CommandID menuID, int x, int y)
{ISelectionService selectionService = host.GetService(typeof(ISelectionService)) as ISelectionService;// get the primary componentIComponent primarySelection = selectionService.PrimarySelection as IComponent;// if the he clicked on the same component again then just show the context// menu. otherwise, we have to throw away the previous// set of local menu items and create new ones for the newly// selected componentif (lastSelectedComponent != primarySelection){// remove all non-global menu items from the context menuResetContextMenu();// get the designerIDesigner designer = host.GetDesigner(primarySelection);// not all controls need a desingerif(designer!=null){// get designer's verbsDesignerVerbCollection verbs = designer.Verbs;foreach (DesignerVerb verb in verbs){// add new menu items to the context menuCreateAndAddLocalVerb(verb);}}}// we only show designer context menus for controlsif(primarySelection is Control){Control comp = primarySelection as Control;Point pt = comp.PointToScreen(new Point(0, 0));contextMenu.Show(comp, new Point(x - pt.X, y - pt.Y));}// keep the selected component for next timelastSelectedComponent = primarySelection;
}

此方法负责显示所选对象的设计器的上下文菜单。如图 11 所示,该方法从选择服务获取所选组件,从主机获取其设计器,从设计器获取动词集合,然后将菜单项添加到每个谓词的上下文菜单中。添加谓词后,将显示上下文菜单。请注意,为设计器谓词创建新菜单项时,也会为菜单项附加一个单击处理程序。自定义单击处理程序处理所有菜单项的单击事件(请参阅示例应用程序中的菜单图标处理程序)。

当用户从设计器上下文菜单中选择菜单项时,将调用自定义处理程序来执行与菜单项关联的谓词。在处理程序中,检索与菜单项关联的谓词并调用它。

ITypeDescriptorFilterService

我前面提到TypeDescriptor类是一个实用程序类,用于获取有关类型的属性、属性和事件的信息。IType描述符筛选器服务可以筛选站点组件的此信息。Type 描述符类在尝试返回已站点组件的属性、属性和/或事件时使用IType描述符信息工具服务。想要修改其设计组件的设计时环境可用的元数据的设计者可以通过实现 IDesignerFilter来做到这一点。IType 描述符筛选器服务定义了三种方法,允许设计器筛选器挂钩和修改已站点组件的元数据。实现 IType 描述符筛选器服务简单直观(TypeDescriptorFilterService.cs应用程序中的一个示例)。

综合起来

如果您已经查看了示例应用程序并运行了窗体设计器,您可能想知道所有服务是如何走到一起的。不能增量地构建窗体设计器,也就是说,您不能实现一个服务,测试应用程序,然后编写另一个服务。您必须实现所有必需的服务,构建用户界面,并将它们绑在一起,然后才能测试应用程序。这是坏消息好消息是,我已经完成了我实施的服务中大部分工作。剩下的就是有点制作。

首先,查看设计器主机的CreateComponent方法。创建新组件时,必须查看它是否为第一个组件(如果 rootComponentnull)。如果是第一个组件,您必须为组件创建专用设计器。专用基础设计器是 IRootDesigner,因为设计器层次结构中最顶级的设计者必须是IRootDesigner(参见图 12)。

IDesigner designer = null;
if (_rootComponent == null)
{// set the root component_rootComponent = component;// create the root designerdesigner = (IRootDesigner)TypeDescriptor.CreateDesigner(component, typeof(IRootDesigner));
}
else
{designer = TypeDescriptor.CreateDesigner(component, typeof(IDesigner));
}
if (designer != null)
{// add the designer to the list_designers.Add(component, designer);// initialize the designerdesigner.Initialize(component);
}

现在您知道第一个组件必须为根组件,您如何确保正确的组件是第一个组件?答案是,设计表面最终成为第一个组件,因为您在主窗口初始化例程期间创建此控件(作为Form)(参见图 13)。

private void InitWindow()
{ serviceContainer = new ServiceContainer(); // create host host = new DesignerHostImpl(serviceContainer); AddBaseServices(); Form designSurfaceForm = host.CreateComponent(typeof(Form),null) as Form;// Create the forms designer now that I have the root designer FormDesignerDocumentCtrl formDesigner = new FormDesignerDocumentCtrl(this.GetService(typeof(IDesignerHost))as IDesignerHost, host.GetDesigner(designSurfaceForm) as IRootDesigner); formDesigner.InitializeDocument();formDesigner.Dock=DockStyle.Fill; formDesigner.Visible=true; formDesigner.Focus(); designSurfacePanel.Controls.Add(formDesigner);// I need to subscribe to selection changed events so // that I can update our properties grid ISelectionService selectionService = host.GetService( typeof(ISelectionService)) as ISelectionService; selectionService.SelectionChanged += new EventHandler( selectionService_SelectionChanged); // Activate the host host.Activate();
}

处理根组件是设计器主机、设计时间环境和用户界面之间粘合剂的唯一棘手部分。其余的很容易理解,花一点时间阅读代码。

调试项目

实现窗体设计器不是一项微不足道的练习。关于这个问题的现有文件很少。一旦您确定从哪里开始以及实现哪些服务,调试项目将很痛苦,因为您必须实现一组必需的服务并插入它们,然后才能开始调试其中任何服务。最后,一旦实现所需的服务,您得到的错误消息不会很有帮助。例如,在调用内部设计时间程序集的线路上可能会获得 Null 参考例外,无法调试,因此您只能想知道哪个服务在哪个处失败。

此外,由于设计时基础结构构建在前面讨论的服务模式之上,因此调试服务可能是个问题。减轻调试难题的技术是记录服务请求。在框架中记录查询的服务请求、通过或失败以及从哪个位置调用(利用环境.StackTrace)可能是一个非常有用的调试工具,可以添加到您的武器库中。

结论

我概述了您需要实现的基础服务,以便启动和运行表单设计器。此外,您还了解如何根据应用程序的需求通过更改配置文件来配置工具箱。剩下的就是调整现有服务,并根据您的需求实施其他一些服务。

C# Tailor Your Application by Building a Custom Forms Designer with .NET相关推荐

  1. Windows Identity Foundation-- Windows身份验证基本框架

    因为要做一个SAML2的项目,但是第一次接触SAML,欠缺很多计算机安全基础知识,用英文实在难以理解,想先把他翻成中文再来理解.网上搜了一下,但是有的文章是用机器翻译的,更难理解,例如Claims被翻 ...

  2. Updater Application Block for .NET

    Introduction Do you need to deploy updates to .NET applications across multiple desktops? Would you ...

  3. 《Journal of Building Performance Simulation》期刊介绍(SCI 3区)

    期刊官方网站 期刊投稿网址 期刊投稿小助手 介绍 The Journal of Building Performance Simulation (JBPS) aims to make a substa ...

  4. django模型查询_如何在Django中编写有效的视图,模型和查询

    django模型查询 I like Django. It's a well-considered and intuitive framework with a name I can pronounce ...

  5. July 4th Links: ASP.NET, ASP.NET AJAX, Visual Studio, Silverlight and IIS7

    原文地址: http://weblogs.asp.net/scottgu/archive/2007/07/04/july-4th-links-asp-net-asp-net-ajax-visual-s ...

  6. PyQt v4 - Python Bindings for Qt v4 | Документация

    PyQt v4 - Python Bindings for Qt v4 | Документация PyQt v4 - Python Bindings for Qt v4 Reference Gui ...

  7. dropbox免费容量_免费课程:使用ES6和Dropbox构建费用管理器

    dropbox免费容量 In my previous startup, we used the Dropbox API heavily in our production process. Our p ...

  8. kpatch: dynamic kernel patching

    GitHub - dynup/kpatch: kpatch - live kernel patchinghttps://github.com/dynup/kpatch 目录 Supported Arc ...

  9. 20 个很棒的 jQuery Mobile 教程

    转载请注明:文章转载自:开源中国社区 [http://www.oschina.net] 本文标题:20 个很棒的 jQuery Mobile 教程 本文地址:http://www.oschina.ne ...

最新文章

  1. spring cloud快速搭建
  2. 如何现在就用到 Ubuntu 21.10
  3. awb数据怎么计算_白平衡自己主动(AWB)算法---2,颜色计算
  4. 前端H5怎么切换语言_「自学系列一」HTML5大前端学习路线+视频教程完整版
  5. jQuery框架学习第三天:如何管理jQuery包装集
  6. WEB-移动端图片适配-弹框
  7. win 10 自动删除解压的文件(关闭 Windows defender)
  8. Cousera - Deep Learning - 课程笔记 - Week 15
  9. 2014 抢票工具 纯java
  10. OPENGL纹理贴图作业分享
  11. 复数乘法的交换律、结合律及乘法 对加法的分配律证明过程
  12. 基于Tensorflow实现声纹识别
  13. 华为HCIP之静态路由
  14. ubuntu状态栏消失
  15. 制作启动U盘后出现“CD驱动器”问题
  16. 【Codecs系列】GDR(Gradual Decoder Refresh)帧
  17. SQL注入点判断及万能密码
  18. 基于Java毕业设计疫情社区志愿者组织的资源管理平台源码+系统+mysql+lw文档+部署软件
  19. easyexcel的使用-个人笔记
  20. iOS小技能: OCR 之身份证识别 (正反面) 【 应用场景:物流类型app进行实名认证】

热门文章

  1. 物联网建设中通讯互联层的终极解决方案
  2. MooseFS学习-概述
  3. HTML5——Web Workers
  4. JavaScript 表格专题
  5. JS-数组和函数冒泡排序递归函数
  6. HTML-超链接锚点笔记
  7. 常量指针、指针常量以及指向常量的指针常量
  8. R7-1 新世界 (5 分)
  9. 7-323 逆波兰表达式 (10 分)
  10. MyBatis日志插件:Mybatis Log Plugin——将控制台输出的mybatis日志转化成可执行的sql语句