The Perfect Host: Create And Host Custom Designers With The .NET Framework 2.0 
Dinesh Chandnani - 三月 2006 (MSDN Magazine: C#)
 
 
This article discusses: 
  • Understanding designers and services
  • Creating a hosting form
  • Building and using a toolbox
  • Loading and managing designers

Code download available at: DesignerHosting.exe (官网VS2005)  (VS2010

Contents

 
 
  Version 1.0 of the Microsoft® .NET Framework provided a very flexible design-time architecture, but offered virtually no implemented code to actually create and host designers. All of the hosting logic was implemented in Visual Studio® .NET, requiring third parties to rewrite all of this complex logic. This has now changed; the .NET Framework 2.0 introduces a set of classes that can be used to host designers right out of the box.
 
Figure 1 Runtime 
 
  To understand how .NET Framework designers work, it is important to know how designers are used. A designer is an object that only exists at design time and that connects to an object that normally exists at run time. The framework connects these two objects and provides a conduit for the design-time object to augment the behavior of the run-time object. At run time, a form and a button on that form are connected only through the parent/child relationship between the two controls (see Figure 1). There is no other object that controls the lifetime of these controls.
 
Figure 2  
 
  The picture looks a little more complex at design time. Both the form and the button have designers associated with them. Both objects are also connected to a host container (see Figure 2), which owns these two objects. The host container also provides services—such as selection services to select one or more objects at design time, and UI services for displaying messages, invoking help, and interacting with the development environment—that the objects and designers may use.
  The host container has many responsibilities. It creates components, binding them to designers and providing services to the components and designers it maintains. It loads designers from some sort of persistent state and saves them back to that state. The host container provides logic for undo, clipboard functions, and many other services that the designers rely upon to deliver a robust design-time environment.
Figure 3 Designer Hosting 
 
  The host container can also persist the state of the designer. To do this, it would use a designer loader. The designer loader may, in turn, use serializers to serialize the components, as shown in Figure 3.
 
Extensibility with Services
  The .NET Framework designer architecture is extensible. Critical to this extensibility, services provide the ability to enhance the functionality available to the various designers. A service is any object that can be queried by a type. Typically you would define some abstract class or interface that represents the service and then provide an implementation of that service. You can add services to and remove services from an object called a service container. IDesignerHost, the primary host interface for designers, is a service container. A service is a feature that may be shared between components written by different parties. Because of this, you must follow certain rules when using and creating services.
  Services are not guaranteed. Whenever you ask for a service by calling the GetService method, you must always check to see whether GetService returned a valid object. Not all services are available on all platforms, and services that were once available may not be available in the future. Thus, your code should be written to degrade gracefully, usually by disabling the feature that required the service, in case a service does become unavailable.
  If you add a service, remember to remove it when your designer is disposed. The designer host may destroy and recreate your designer from time to time. If you fail to remove a service, the old designer may be left in memory.
DesignSurface and DesignSurfaceManager
  The .NET Framework 2.0 introduces two classes that are used for hosting designers and providing services to the designers: DesignSurface and DesignSurfaceManager. DesignSurface is what the user perceives as a designer; it is the UI the user manipulates to change design-time features. DesignSurface may be used as a standalone designer or it may be coupled with DesignSurfaceManager to provide a common implementation for an application that hosts multiple DesignSurfaces.
  DesignSurface provides several design-time services automatically (see Figure 4). Most of these can be overridden in the service container. It is illegal to replace the non-replaceable services because their implementations all depend on each other. Note that all services added to the service container that implement IDisposable will be disposed when the design surface is disposed.
 
Figure 4 Default DesignSurface Design-Time Services 

  In addition to the default services, DesignSurface also provides IDictionaryService, available through a component's site. This service, which provides a generic dictionary of key/value pairs that can be used to store arbitrary data about a component, is unique to each component. It is not possible to replace these services because there is no way to replace services on a per-site basis.
  DesignSurfaceManager is intended to be a container of designers. It provides common services that handle event routing between designers, property windows, and other global objects. Using DesignSurfaceManager is optional, but recommended if you intend to have several designer windows.
  DesignSurfaceManager also provides several design-time services automatically (see Figure 5). Each of these can be overridden by replacing their value in the protected ServiceContainer property. As with DesignSurface, all DesignSurfaceManager services added to the service container that implement IDisposable will be disposed when the designer application is disposed.
 

Figure 5 DesignSurfaceManager Design-Time Services

Service Description
IUIService Provides a way for components to show a UI such as error messages and dialog
boxes
IDesignerEventService Provides a global eventing mechanism for designer
events
 
  IDesignerEventService is a particularly useful service. It allows an application to know when a designer becomes active. IDesignerEventService provides a collection of designers and is a single place where global objects, such as the Property window, can listen to selection change events.
 
The Hosting Form
  To demonstrate how simple it is to host a designer, I wrote the following sample code to create a basic Windows® Forms designer and display it: 
// Create the DesignSurface and load it with a form
DesignSurface ds = new DesignSurface();
ds.BeginLoad(typeof(Form)); // Get the View of the DesignSurface, host it in a form, and show it
Control c = ds.View as Control;
Form f = new Form();
c.Parent = f;
c.Dock = DockStyle.Fill;
f.Show();
  In this code snippet, I have loaded the DesignSurface with a Form. Similarly, you can load the DesignSurface with any component that has a root designer available for it. For example, you could load a UserControl or a Component instead.
  The sample provided in the code download for this article hosts four different root components: Form, UserControl, Component, and MyTopLevelComponent (a graph designer). When you run the sample, a shell UI will open. This interface includes a toolbox, a properties browser, a tab control for hosting designers, an output window, and a Solution Explorer, as shown in Figure 6. Selecting File | New | Form from the menu opens a new designer host with Windows Forms Designer. This essentially uses the code I've just shown you to load the designer. Rather than loading a Form, the sample app illustrates how to load a UserControl or Component.
 
Figure 6 Hosting Windows Forms Designer 
 
  To create a root component, create a designer that implements IRootDesigner and then associate this designer with your component. The View property of the root component specifies the view that will be presented to the user.
  One of the main services provided by DesignSurface is IDesignerHost. It is the master interface used to provide designers and controls access to types, services, and transactions. It can also be used to create and destroy components. To add a button to the Windows Forms designer I created earlier, all I need to do is get the IDesignerHost service from the DesignSurface and use it to create the button as shown in Figure 7.
 
Figure 7 Create a Button with IDesignerHost
// Add a Button to the Form
IDesignerHost idh = (IDesignerHost)ds.GetService(typeof(IDesignerHost));
Button b = (Button)idh.CreateComponent(typeof(Button)); // Set the Parent of this Button to the RootComponent (the Form)
b.Parent = (Form)idh.RootComponent; // Use ComponentChangeService to announce changing of the
// Form's Controls collection */
IComponentChangeService icc = (IComponentChangeService)
idh.GetService(typeof(IComponentChangeService));
icc.OnComponentChanging(idh.RootComponent,
TypeDescriptor.GetProperties(idh.RootComponent)["Controls");
  IToolboxUser specifies that the designer supports adding controls from the toolbox. This means that if you do have a toolbox that implements ToolboxService, you can use the IToolboxUser interface to add controls to the root component. For instance:
/* Add a Button to the Form using IToolboxUser */
IDesignerHost idh = (IDesignerHost)ds.GetService(typeof(IDesignerHost));
IToolboxUser itu = (IToolboxUser)idh.GetDesigner(idh.RootComponent);
itu.ToolPicked(new ToolboxItem(typeof(Button)));
  When controls are added to the custom RootDesigner in the sample application by double-clicking the items in the toolbox, the view of the RootDesigner updates to display a pie chart as shown in Figure 8. Clicking on the GraphStyle link changes the view to a bar graph.
 
Figure 8 Custom RootDesigner Updates 
The Toolbox
  MyRootDesigner implements the IToolboxUser interface. This has two methods: GetToolSupported and ToolPicked. You can use GetToolSupported to filter the items that can be added onto the designer. ToolPicked eventually calls into the CreateComponents method (which, as the name implies, is responsible for creating the components) of ToolboxItem.
  Now that I have added the controls and components to the designer, let's take a closer look at how to implement a toolbox. First, your toolbox needs to implement IToolboxService—this service is added to the service container and can be accessed by anyone who needs to use it. The main functions of IToolboxService are shown in Figure 9.
 
Figure 9 IToolboxService Members (Close Figure 9)

Method or Property Description
GetSelectedToolboxItem Returns the selected toolbox item
SerializeToolboxItem Creates a binary serialized object (DataObject) from ToolboxItem
DeserializeToolboxItem Returns the ToolboxItem from a binary serialized object
(DataObject)
  To allow the items from the toolbox to be added onto the designer using the mouse or keyboard, the toolbox in the sample hooks onto the KeyDown and MouseDown events. For the Enter key or a mouse double-click, IToolboxUser.ToolPicked is called. The sample shows how to serialize the ToolboxItem into DataObject and DoDragDrop for when a mouse single-click event occurs. IToolboxService.SerializeToolboxItem will be called on mouse up and the item will be added to the designer.
  When a new control or component is added to the designer, you can provide a custom name for the control by implementing INameCreationService. The sample application shows an example of this service in action using CreateName, ValidateName, and IsValidName.
Multiple DesignSurfaces
  When managing multiple DesignSurfaces, it is a good idea to use a DesignSurfaceManager. This makes it easier to manage these DesignSurfaces. (Note that the services of DesignSurfaceManager are also available to DesignSurface.)
  Calling DesignSurfaceManager.CreateDesignSurface calls into CreateDesignSurfaceCore. You can override this function to create a custom DesignSurface and add the services. The sample app creates a custom HostSurface by overriding this function in the HostSurfaceManager class:
protected override DesignSurface CreateDesignSurfaceCore(
IServiceProvider parentProvider)
{
return new HostSurface(parentProvider);
}
  You can then hook into the ActiveDesignSurfaceChanged event and update the output window in the HostSurfaceManager class, as shown here:
void HostSurfaceManager_ActiveDesignSurfaceChanged(
object sender, ActiveDesignSurfaceChangedEventArgs e)
{
ToolWindows.OutputWindow o =
this.GetService(typeof(ToolWindows.OutputWindow)) as
ToolWindows.OutputWindow;
o.RichTextBox.Text += "New host added./n";
}
  DesignerLoaders
  So far I've created DesignSurfaces, hosted designers, added controls, implemented a toolbox, and added and accessed services like OutputWindow. The next step is to persist the designer. The designer loader is, as you'd expect, responsible for loading the designer from some persistent state. Simple and flexible, designer loaders have very few requirements. In fact, you can create an instance of the Windows Forms designer with a one-line designer loader that simply creates an instance of System.Windows.Forms.Form.
  In addition to loading a form design, a designer loader is also responsible for saving the design. Because saving is an optional behavior, a designer loader listens to change events from the designer host and automatically saves state according to these events.
  The .NET Framework 2.0 introduces two new classes for writing custom loaders: BasicDesignerLoader and CodeDomDesignerLoader. The sample app illustrates implementations of both loader types. Earlier I demonstrated loading the DesignSurface with a root component by passing in the type of the component. However, if you are using a loader, it should be used to load the design surface. The BeginLoad code snippet you'll need when using loaders should look something like this:
// Load it using a Loader
ds.BeginLoad(new MyLoader());
  The DesignerLoader is responsible for loading the root component in the DesignSurface and creating any components. When creating a new form or any other root component, the loader simply loads it. In comparison, when loading from a code file or some other store, the loader is responsible for parsing the file or store and recreating the root component along with any other necessary components.
  The .NET Framework defines an abstract base class called DesignerLoader that is used to load and save designers from persistent storage. The base class is abstract so that any type of persistence model can be used. This, however, adds to the complexity of implementing the class.
  BasicDesignerLoader provides a complete and common implementation of a designer loader minus any information related to the persistence format. Like DesignerLoader, it is abstract, not dictating anything about the persistence format. BasicDesignerLoader does, however, handle the standard work of knowing when to save, knowing how to reload, and tracking change notifications from designers. Its features include support for multiple load dependencies, tracking of the modified bit to indicate a need to save changes, and deferred idle time reload support.
  The services shown in Figure 10 are added to the designer host's service container by BasicDesignerLoader. As with other services, you can change replaceable services by editing their value in the protected LoaderHost property.The sample app implements a BasicDesignerLoader that persists the state in an XML format. To see how it works, select File | Type | BasicDesignerLoader. Then create a new Form by selecting File | New | Form. To see the XML code that is generated, select View | Code | XML. The XML code that is generated by the app will look something like this:
 
<Object type="System.Windows.Forms.Form, System.Windows.Forms,
Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
name="Form1" children="Controls">
<Property name="Name">Form1</Property>
<Property name="DataBindings">
<Property name="DefaultDataSourceUpdateMode">OnValidation</Property>
</Property>
<Property name="ClientSize">292, 273</Property>
</Object>
PerformFlush and PerformLoad are the two abstract functions of the BasicDesignerLoader that you need to implement for serializing and deserializing, respectively.
 
Figure 10 BasicDesignerLoader Services
 
 
CodeDomDesignerLoader
  Design-time serialization is handled by generating source code. One of the challenges of any code-generation scheme is the handling of multiple languages. The .NET Framework is designed to work with a variety of languages, so I also want the designers to emit to several languages. There are two ways to address this. The first is to require each language vendor to write the code-generation engine for their language. Unfortunately, no language vendor can anticipate the wide variety of code-generation requirements that third-party component vendors may need. The second approach is to require each component vendor to provide code generation for each language they want to support. This is equally bad, because the number of languages supported is not fixed.
  To solve this problem, the .NET Framework defines an object model called the Code Document Object Model (CodeDOM). All source code can essentially be broken down into primitive elements and the CodeDOM is an object model for those elements. When code adheres to CodeDOM, the generated object model can later be sent to a code generator for a particular language to render the appropriate code.
  The .NET Framework 2.0 introduces CodeDomDesignerLoader, which inherits from BasicDesignerLoader. The CodeDomDesignerLoader is a full loader that works by reading and writing CodeDOM. It is a turnkey designer loader, so all you need to do is provide the CodeDOM.
  In the sample app you can select File | Type | CodeDomDesigner-Loader to see an example of the CodeDOM in action. Create a new form by selecting File | New | Form—this creates a DesignSurface and loads it using a CodeDomDesignerLoader. To check the code, select View | Code | C# to see the C# version of the form's code, or View | Code | VB to see the Visual Basic® version.
  To generate the code, the sample app uses CSharpCodeProvider and VBCodeProvider. It also uses the code providers to compile the code and run the executable (see Figure 11).
Figure 11 Generate, Compile, and Run
CompilerParameters cp = new CompilerParameters();
AssemblyName[] assemblyNames =
Assembly.GetExecutingAssembly().GetReferencedAssemblies(); foreach (AssemblyName an in assemblyNames)
{
Assembly assembly = Assembly.Load(an);
cp.ReferencedAssemblies.Add(assembly.Location);
} cp.GenerateExecutable = true;
cp.OutputAssembly = executable; cp.MainClass = "DesignerHostSample." +
this.LoaderHost.RootComponent.Site.Name; // Compile CodeCompileUnit using CodeProvider
CSharpCodeProvider cc = new CSharpCodeProvider();
CompilerResults cr = cc.CompileAssemblyFromDom(cp, codeCompileUnit); if (cr.Errors.HasErrors)
{
string errors = string.Empty;
foreach (CompilerError error in cr.Errors)
{
errors += error.ErrorText + "/n";
}
MessageBox.Show(errors, "Errors during compile.");
}
  ITypeResolutionService, which is required when using the CodeDomDesignerLoader, is responsible for resolving a type. One scenario, for example, where this service is called to resolve types is when adding a control from the toolbox into the designer. The sample application resolves all types from the System.Windows.Forms assembly so that you can add controls from the Windows Forms tab of the toolbox.
Conclusion
  As you've seen, the .NET Framework provides a powerful and flexible designer hosting infrastructure. Designers provide straightforward extensibility that help to address very specific needs or more advanced scenarios than were supported by previous versions of Visual Studio. Of course, designers can be hosted easily outside Visual Studio, as well. Be sure to download the sample application, so you can play around with the code and start implementing your own custom designers today.
 
------------------------------------------------------------------------
Dinesh Chandnani is a Software Design Engineer in Test for the .NET Client Team at Microsoft, working on designer hosting and other designer features. He started working for Microsoft in 2002 after completing his masters' degree in Computer Science at the University of Arizona.
(src:http://msdn.microsoft.com/zh-cn/magazine/cc163634(en-us).aspx)

DesignSurface简介的更多相关文章

  1. ASP.NET Core 1.1 简介

    ASP.NET Core 1.1 于2016年11月16日发布.这个版本包括许多伟大的新功能以及许多错误修复和一般的增强.这个版本包含了多个新的中间件组件.针对Windows的WebListener服 ...

  2. MVVM模式和在WPF中的实现(一)MVVM模式简介

    MVVM模式解析和在WPF中的实现(一) MVVM模式简介 系列目录: MVVM模式解析和在WPF中的实现(一)MVVM模式简介 MVVM模式解析和在WPF中的实现(二)数据绑定 MVVM模式解析和在 ...

  3. Cassandra简介

    在前面的一篇文章<图形数据库Neo4J简介>中,我们介绍了一种非常流行的图形数据库Neo4J的使用方法.而在本文中,我们将对另外一种类型的NoSQL数据库——Cassandra进行简单地介 ...

  4. REST简介

    一说到REST,我想大家的第一反应就是“啊,就是那种前后台通信方式.”但是在要求详细讲述它所提出的各个约束,以及如何开始搭建REST服务时,却很少有人能够清晰地说出它到底是什么,需要遵守什么样的准则. ...

  5. Microservice架构模式简介

    在2014年,Sam Newman,Martin Fowler在ThoughtWorks的一位同事,出版了一本新书<Building Microservices>.该书描述了如何按照Mic ...

  6. const,static,extern 简介

    const,static,extern 简介 一.const与宏的区别: const简介:之前常用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量. 执行时刻:宏是预编 ...

  7. HTTPS简介

    一.简单总结 1.HTTPS概念总结 HTTPS 就是对HTTP进行了TLS或SSL加密. 应用层的HTTP协议通过传输层的TCP协议来传输,HTTPS 在 HTTP和 TCP中间加了一层TLS/SS ...

  8. 【Machine Learning】机器学习及其基础概念简介

    机器学习及其基础概念简介 作者:白宁超 2016年12月23日21:24:51 摘要:随着机器学习和深度学习的热潮,各种图书层出不穷.然而多数是基础理论知识介绍,缺乏实现的深入理解.本系列文章是作者结 ...

  9. Cesium简介以及离线部署运行

    Cesium简介 cesium是国外一个基于JavaScript编写的使用WebGL的地图引擎,一款开源3DGIS的js库.cesium支持3D,2D,2.5D形式的地图展示,可以自行绘制图形,高亮区 ...

随机推荐

  1. Log4j官方文档翻译(五、日志输出的方法)

    日志类提供了很多方法用于处理日志活动,它不允许我们自己实例化一个logger,但是提供给我们两种静态方法获得logger对象: public static Logger getRootLogger() ...

  2. Log4j官方文档翻译(四、如何在java中输出日志消息)

    我们已经创建来配置文件,本章详细的介绍下如何生成调试信息,并把他们转化成文本文件. 基本的例子 下面就是创建的一个基本的例子: log4j.properties的内容为: log = /usr/hom ...

  3. loj 300 [CTSC2017]吉夫特 【Lucas定理 + 子集dp】

    题目链接 loj300 题解 orz litble 膜完题解后,突然有一个简单的想法: 考虑到\(2\)是质数,考虑Lucas定理: \[{n \choose m} = \prod_{i = 1} { ...

  4. BZOJ3166 [Heoi2013]Alo 【可持久化trie树 + 二分 + ST表】

    题目 Welcome to ALO ( Arithmetic and Logistic Online).这是一个VR MMORPG , 如名字所见,到处充满了数学的谜题. 现在你拥有n颗宝石,每颗宝石 ...

  5. 如何通过IP地址分辨公网、私网、内网、外网

    如何通过IP地址分辨公网.私网.内网.外网   内.外网是相对于防火墙而言的,在防火墙内部叫做内网,反之就是外网.   在一定程度上外网等同于公网,内网等同于私网.   地址为如下3个区域就是处于私网 ...

  6. kubernetes-dashboard

    1.导入kubernetes-dashboard 镜像 [root@node1 DNS]# docker load < kube-dashboard.tar 6bc90c4dba69: Load ...

  7. SQLalchemy 使用记录

    1.models.py中添加该方法,可通过该方法转dict #驼峰 def to_hump_dict(self): return {commonUtils.str2Hump(c.name): geta ...

  8. iterator & iterable

    一. java.lang.Iterable java.util.Iterator Iterator是迭代器类,而Iterable是接口. 好多类都实现了Iterable接口,这样对象就可以调用iter ...

  9. Linux 命令行下使用多行输入

    比较简单,建议实操,直接上图: 一行结束,直接敲回车换行.上一个例子,输入eof,终止多行输入:下一个例子,输入done,终止多行 ~~ 如果是参数太多,一行输入不完,可以通过 "空格\en ...

  10. 5种你未必知道的JS和CSS交互的方法

    随着浏览器不断的升级改进,CSS和JavaScript之间的界限越来越模糊.本来它们是负责着完全不同的功能,但最终,它们都属于网页前端技术,它们需要相互密切的合作.我们的网页中都有.js文件和.css ...