在我开始这个项目之前,有几个基本概念很重要。让我们从设计器的定义开始。设计器提供设计模式 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
具有一个导航栏,其中包含多个组,每个组都包含工具。若要生成工具箱,必须执行以下操作:
IToolbox
服务IToolbox
服务实现插入设计时间环境对于任何实际应用程序,构建工具箱用户界面可能非常耗时。您必须做出的第一个设计决策是如何发现和加载工具,并且有几种可行的方法。使用第一种方法,您可以硬编码要显示的工具。不建议这样做,除非您的应用程序非常简单,并且将来需要很少的维护。
第二种方法涉及从配置文件中读取工具。例如,工具可以定义如下:
<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 所示。
///
/// used to load the toolbox
///
///
public void LoadToolbox(FdToolbox tools)
{
// clear out existing nodes and the imageList associated with the tree
toolboxView.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 now
treeViewImgList.Images.Add(requiredImgList.Images[0]);
treeViewImgList.Images.Add(requiredImgList.Images[1]);
// assign imageList to the treeView
toolboxView.ImageList = treeViewImgList;
// if we have categories...
if (tools?.FdToolboxCategories == null || tools.FdToolboxCategories.Length <= 0) return;
foreach (Category category in tools.FdToolboxCategories) LoadCategory(category);
}
///
/// loads a group of toolbox items into the under the given category
///
///
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 category
TreeNode catNode = new TreeNode(category.DisplayName) { ImageIndex = 0, SelectedImageIndex = 0 };
// add this category to the tree
toolboxView.Nodes.Add(catNode);
// every category gets the selection tool node
AddSelectionNode(catNode);
foreach (FdToolboxItem item in category.FdToolboxItem) LoadItem(item, catNode);
}
///
/// loads an item into the tree
///
///
///
private void LoadItem(FdToolboxItem item, TreeNode cat)
{
if (item?.Type == null || cat == null) return;
// load the type
string[] assemblyClass = item.Type.Split(',');
Type toolboxItemType = GetTypeFromLoadedAssembly(assemblyClass[0], assemblyClass[1]);
//
ToolboxItem toolItem = new ToolboxItem(toolboxItemType);
// get the image for the item
Image img = GetItemImage(toolboxItemType);
// create the node for it
TreeNode nd = new TreeNode(toolItem.DisplayName);
// add the item's bitmap to the image list
if (img != null)
{
// add it to the image list
treeViewImgList.Images.Add(img);
// set the nodes image index
nd.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 node
cat.Nodes.Add(nd);
}
///
/// finds the image associated with the type
///
///
///
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
定义方法和事件。在主机实现中,您为主机服务提供实现以及其他几个服务。这些应该包括IContainer
、IComponentChangeService
、IExtenderProviderService
、ITypeDescriptionFilterService
和 IDesignerEventService
。
设计者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 component
if (component == null)
throw new ArgumentException("component");
// if we don't have a name, create one
if (name == null || name.Trim().Length == 0)
{
// we need the naming service
if (!(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 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 we don't have
// to do it here...
component.Site.Name = name;
// bail out
return;
}
// create a site for the component
ISite site = new SiteImpl(component, name, this);
// create component-site association
component.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 event
if (ComponentAdding != null)
{
try
{
ComponentAdding(this, evtArgs);
}
catch { }
}
// if this is the root component
IDesigner designer = null;
if (_rootComponent == null)
{
// set the root component
_rootComponent = component;
// create the root designer
designer = (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 designer
designer.Initialize(component);
}
// add to container component list
_sites.Add(site.Name, site);
// now fire off added event
if (ComponentAdded == null) return;
try
{
ComponentAdded(this, evtArgs);
}
catch { }
}
如果您忽略检查和事件,可以总结添加算法,如下所示。首先,为该类型创建一个新的IComponent
,为组件创建一个新的ISite
。这将建立站点到组件的关联。请注意,站点的构造函数接受设计器主机实例。站点构造函数采用设计器主机和组件,以便它可以建立图 2 中所示的组件-容器关系。然后创建、初始化组件设计器,并添加到组件到设计器字典中。最后,新组件将添加到设计器主机容器中。
删除组件需要一些清理。同样,忽略简单的检查和验证,删除操作相当于删除设计器,释放设计器,删除组件的站点,然后释放组件。
设计器事务的概念类似于数据库事务,因为它们都对一系列操作进行分组,以便将组视为工作单元并启用提交/中止机制。设计器事务在整个设计时基础结构中使用,以支持取消操作,并启用视图延迟其显示的更新,直到整个事务完成。设计器主机提供通过IDesignerHost
接口管理设计器事务的工具。管理事务不是非常困难(请参阅DesignerTransactionImpl.cs
应用程序中的一个示例)。
设计器事务,代表事务中的单个操作。当要求主机创建事务时,它会创建一个Designer
事务Impl
的实例来管理单个更改。当Designer
事务Impl
的实例管理每个更改时,主机跟踪事务。如果不实现事务管理,则在使用窗体设计器时,会得到一些有趣的异常。
正如我说过的,组件被放入容器中,以进行终身管理,并为他们提供服务。设计器主机接口IDesignerHost
定义了创建和删除组件的方法,因此,如果主机提供此服务,您就不应该感到惊讶。同样,容器服务定义添加和删除组件的方法,这些方法与IDesignerHost
的Create
组件和销毁组件方法重叠。因此,大部分繁重的工作都是在容器的添加和删除方法中完成的,创建和销毁方法只需将调用转发到这些方法。
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 event
if (SelectionChanging != null)
{
try
{
SelectionChanging(this, EventArgs.Empty);
}
catch
{
// ignored
}
}
// don't allow an empty collection
if (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 removed
IEnumerator 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 event
if (SelectionChanged == null) return;
try
{
SelectionChanged(this, EventArgs.Empty);
}
catch
{
// ignored
}
}
ISelectionService
跟踪设计器表面上的组件选择。其他服务(如IMenuCommandService
)在需要获取有关所选组件的信息时使用此服务。为了提供此信息,服务维护一个内部列表,表示当前选定的组件。设计时环境调用SetSelectedComponents
,当对组件的选择进行了更改时,它具有组件的集合。例如,如果用户选择一个组件,然后关闭 shift 键并选择另外三个组件,则对每次添加到选择列表的每个组件都调用该方法。每次调用该方法时,设计时环境会告诉我们哪些组件受到影响以及如何(通过选择类型枚举)。实现着眼于如何更改组件,以确定是否需要向内部选定列表中添加或删除组件。修改内部选择列表后,我将调用"选择更改"事件(SelectionServiceImpl.cs
中selectionService_SelectionChanged
方法更改),以便可以使用新选择更新属性网格。应用程序的主要窗体MainWindow
订阅选择服务的选择更改事件,以便使用所选组件更新属性网格。
另请注意,选择服务定义主选择属性。主选择始终设置为选择的最后一个项目。当我谈论显示正确的设计器上下文菜单时,我将使用此属性讨论IMenuCommandService
。
选择服务是最难正确实现的服务之一,因为它具有一些使实现复杂化的宝贵功能。例如,在实际应用程序中,处理键盘事件(如 Ctrl+A)以及管理有关处理大型选择列表的问题是有意义的。
ISite 实现是更重要的实现之一,如图 9 所示。
///
/// Summary description for SiteImpl.
///
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 Members
public IComponent Component => _component;
public IContainer Container => _host.Container;
public bool DesignMode => true;
public string Name
{
get => _name;
set
{
// null name is not valid
if (value == null)
throw new ArgumentException("value");
// if we have the same name
if (string.Compare(value, _name, false) == 0) return;
// make sure we have a valid name
INameCreationService 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 name
string oldName = _name;
// set the new name
MemberDescriptor md = TypeDescriptor.CreateProperty(_component.GetType(), "Name", typeof(string), new Attribute[] {});
// fire changing event
hostImpl.OnComponentChanging(_component, md);
// set the value
_name = value;
// we also have to fire the rename event
_host.OnComponentRename(_component,oldName,_name);
// fire changed event
hostImpl.OnComponentChanged(_component, md, oldName, _name);
}
}
#endregion
#region IServiceProvider Members
public object GetService(Type service)
{
return service == typeof(IDictionaryService) ? this : _host.GetService(service);
// forward request to the host
}
#endregion
#region IDictionaryService Implementation
public 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 component
IComponent 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 component
if (lastSelectedComponent != primarySelection)
{
// remove all non-global menu items from the context menu
ResetContextMenu();
// get the designer
IDesigner designer = host.GetDesigner(primarySelection);
// not all controls need a desinger
if(designer!=null)
{
// get designer's verbs
DesignerVerbCollection verbs = designer.Verbs;
foreach (DesignerVerb verb in verbs)
{
// add new menu items to the context menu
CreateAndAddLocalVerb(verb);
}
}
}
// we only show designer context menus for controls
if(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 time
lastSelectedComponent = primarySelection;
}
此方法负责显示所选对象的设计器的上下文菜单。如图 11 所示,该方法从选择服务获取所选组件,从主机获取其设计器,从设计器获取动词集合,然后将菜单项添加到每个谓词的上下文菜单中。添加谓词后,将显示上下文菜单。请注意,为设计器谓词创建新菜单项时,也会为菜单项附加一个单击处理程序。自定义单击处理程序处理所有菜单项的单击事件(请参阅示例应用程序中的菜单图标处理程序)。
当用户从设计器上下文菜单中选择菜单项时,将调用自定义处理程序来执行与菜单项关联的谓词。在处理程序中,检索与菜单项关联的谓词并调用它。
我前面提到TypeDescriptor
类是一个实用程序类,用于获取有关类型的属性、属性和事件的信息。IType
描述符筛选器服务可以筛选站点组件的此信息。Type 描述符类在尝试返回已站点组件的属性、属性和/或事件时使用IType
描述符信息工具服务。想要修改其设计组件的设计时环境可用的元数据的设计者可以通过实现 IDesignerFilter
来做到这一点。IType
描述符筛选器服务定义了三种方法,允许设计器筛选器挂钩和修改已站点组件的元数据。实现 IType
描述符筛选器服务简单直观(TypeDescriptorFilterService.cs
应用程序中的一个示例)。
如果您已经查看了示例应用程序并运行了窗体设计器,您可能想知道所有服务是如何走到一起的。不能增量地构建窗体设计器,也就是说,您不能实现一个服务,测试应用程序,然后编写另一个服务。您必须实现所有必需的服务,构建用户界面,并将它们绑在一起,然后才能测试应用程序。这是坏消息好消息是,我已经完成了我实施的服务中大部分工作。剩下的就是有点制作。
首先,查看设计器主机的CreateComponent
方法。创建新组件时,必须查看它是否为第一个组件(如果 rootComponent
为null
)。如果是第一个组件,您必须为组件创建专用设计器。专用基础设计器是 IRootDesigner
,因为设计器层次结构中最顶级的设计者必须是IRootDesigner
(参见图 12)。
IDesigner designer = null;
if (_rootComponent == null)
{
// set the root component
_rootComponent = component;
// create the root designer
designer = (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 designer
designer.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)可能是一个非常有用的调试工具,可以添加到您的武器库中。
我概述了您需要实现的基础服务,以便启动和运行表单设计器。此外,您还了解如何根据应用程序的需求通过更改配置文件来配置工具箱。剩下的就是调整现有服务,并根据您的需求实施其他一些服务。