2005年10月08日

volatile —多线程程序员的最好朋友  
           作者:Andrei Alexandrescu
           原文:http://www.cuj.com/documents/s=7998/cujcexp1902alexandr/

——————————————————————————–
   就像广为人知的const一样,volatile也是一个类型标志符。它与那些需要在不同线程中访问和修改的变量一起使用。没有volatile,即使不能说编写多线程程序是不可能的,但使编译器浪费大量的优化机会却是不争的事实。下面让我一一道来。
   考虑下面的代码:
class Gadget
{
public:
    void Wait()
    {
        while (!flag_)
        {
            Sleep(1000); // sleeps for 1000 milliseconds
        }
    }
    void Wakeup()
    {
        flag_ = true;
    }
    …
private:
    bool flag_;
};
上面Gadget::Wait的目标是每秒检查flag_成员变量,在另一个线程把flag_设置为true时返回,最少编写这段代码的程序员是这个意思,但是,这是不正确的。
假设编译器发现Sleep(1000)调用的是外部库的函数并且不可能修改成员变量flag_,编译器觉得自己应该把flag_缓存到寄存器中,每次访问这个变量都访问寄存器而不是慢速的内存,这对单线程代码是一个优秀的优化设计,但在这种情况下,它损害了正确性:在你调用Wait后,尽管另一个线程调用了Wakeup,Wait还是会永远循环下去。这是因为flag_的变化并不会反映到缓存flag_的寄存器中。这个优化有点……"太优化"了。

把变量缓存到寄存器中在大多数时间是一个非常有价值的优化,浪费掉它是不可取的。C和C++给了你一个机会,通过显式的声明来禁止这种缓存。如果你用volatile标志符来修饰一个变量,编译器就不会把这个变量缓存到寄存器中-每次访问这个变量都会访问它在内存中的实际位置。这样,要使Gadget的Wait/Wakeup能够工作,你需要做的就只是正确的标识flag_。 

class Gadget
{
public:
    … as above …
private:
    volatile bool flag_;
};
volatile的基本原理和使用就解释到这儿,建议你在多线程编程时用volatile标识基本类型变量。然而,你还可以用volatile做多得多的事情,因为它是C++的丰富多彩的类型系统的一部分。

对用户自定义类型使用volatile
你不仅可以用volatile标识基本类型,也可以用它标识用户自定义类型,在这种情况下,volatile使用和const类似的方式修饰变量。(你可以同时使用const和volatile来修饰一个变量)
与const不同,volatile区分基本类型和用户自定义类型,也就是说,与类不同,在被volatile修饰时,基本类型还支持所有与其相关的操作(加,乘,赋值等)。例如,你可以把一个非volatile的整型变量赋给一个volatile的整型变量,但是你不能把一个非volatile对象赋给一个volatile对象。
我们来看一个volatile用于用户自定义类型的例子

class Gadget
{
public:
    void Foo() volatile;
    void Bar();
    …
private:
    String name_;
    int state_;
};

Gadget regularGadget;
volatile Gadget volatileGadget;
如果你认为volatile不是那么有用的话,就准备着被吓一跳吧

volatileGadget.Foo(); // ok, volatile对象调用volatile方法
regularGadget.Foo();  // ok, 非volatile对象调用volatile方法
volatileGadget.Bar(); // 错误! volatile对象不能调用非volatile方法!

从非volatile类型变换到volatile类型是隐含的,但是,与const一样,你不能直接把一个volatile类型转换成非volatile类型,在这种情况下,你必须使用强制转换

Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok

一个volatile标识的类只把访问权限给与自己接口的一个子集,一个在实现者的控制之下的一个子集。用户只有使用一个const_cast才能得到对这个类型接口的完全访问权限。另外,与const相似,volatile从类扩展到类的成员(例如,volatileGadget.name_和volatileGadget.state_ 就是volatile变量)

volatile,关键区和竞争条件
在多线程编程中最简单也是最常用的同步机制是mutex。一个mutex向外提供Acquire和Realse两个方法,一旦你在某个线程中调用了Acquire方法,其它线程调用Acquire方法将会被阻塞。以后,当这个线程调用Release后,以前阻塞的线程中的一个的Acquire调用将会被释放,也就是说,对于一个mutex,在调用Acquire和Release之间的代码,同时只有一个线程能够获得处理器时间,在调用Acquire和调用Release之间的代码叫做一个关键区。(Windows的术语稍微有些混淆,它把mutex自己叫做关键区,而实际上mutex只是进程之间的互斥锁,如果它们被称为线程互斥体或进程互斥体的话就比较明确了)

mutex用来在线程或进程竞争过程中保护数据。根据定义,当多个线程对数据的操作结果取决于线程的调度过程时就会发生竞争。当两个或多个线程要求使用同一份数据时,竞争就出现了。因为线程相互之间可以在一个随机的时间中断对方,这样数据就可能被损坏,所以,更改数据,甚至在有些情况下访问数据都必须用关键区小心地保护起来。在面向对象编程中,这往往意味着你把一个mutex存在一个类的成员变量里,任何时候你访问类的状态时,都要首先使用它。

有经验的多线程程序员可能觉得上面两段很无聊,但他们的目的是提供一个智力测验,因为我们现在就要联系volatile,我们这样做就引出了C++类型和线程术语这两个看似相互无关的东西。

在关键区外部,任何线程都可以在任何时间中断其它线程,没有什么控制,这样多个线程都可以访问的变量的值是不可预测的。这就是最初提出volatile的意图-阻止编译器无意的缓存多线程使用的变量的值。

在由mutex定义的关键区内部,同时只有一个线程可以访问。所以,在关键区内部的代码,具有但线程的语境,变量不再是不可预测的了-你可以去掉volatile标识符。简单的说,多线程间共享的数据,在关键区外是不可预测的,而在关键区内则不是。

你可以通过锁住一个mutex进入一个关键区,通过使用const_cast去掉一个类型的volatile标识符。如果我们试着把这两个操作放在一起,我们就在c++的类型系统和应用程序的线程术语之间建立了一个连接,我们可以让编译器为我们检查竞争。

LockingPtr
我们需要一个工具来收集mutex和const_cast。我们来写一个LockingPtr的类模版,用一个volatile对象obj和一个mutex mtx初始化,在他的生命周期内,LockingPtr保持着要求mtx的状态。LockingPtr也提供到剥离了volatile标识符的对象的访问。这个访问使用一个安全指针,通过操作符->和*来访问。const_cast在LockingPtr内部执行。因为LockingPtr在自己的生命周期内都保持着木特性,这个强制转换在语法上是合法的。

首先,我们定义Mutex类的骨架

class Mutex
{
public:
    void Acquire();
    void Release();
    …   
};

为了使用LockingPtr,你必须用本地操作系统的数据结构和函数实现Mutex。
LockingPtr是以要控制的变量的类型为参数的模版,例如,如果你想要控制Widget,使用LockingPtr<Widget>,然后用一个volatile类型的Widget变量初始化。

LockingPtr的定义非常简单。LockingPtr实现了一个简单的安全指针,他只是简单地收集const_cast和关键区。

template <typename T>
class LockingPtr {
public:
   // Constructors/destructors
   LockingPtr(volatile T& obj, Mutex& mtx)
       : pObj_(const_cast<T*>(&obj)),
        pMtx_(&mtx)
   {    mtx.Lock();    }
   ~LockingPtr()
   {    pMtx_->Unlock();    }
   // Pointer behavior
   T& operator*()
   {    return *pObj_;    }
   T* operator->()
   {   return pObj_;   }
private:
   T* pObj_;
   Mutex* pMtx_;
   LockingPtr(const LockingPtr&);
   LockingPtr& operator=(const LockingPtr&);
};
虽然很简单,LockingPtr对于书写正确的多线程代码很有帮助。你应该把多线程之间共享的对象定义为volatile,并且任何时候都不对他们使用const_cast-总是使用LockingPtr,我们下面用一个例子说明。
假设你有两个线程共享 vector<char> 对象:
class SyncBuf {
public:
    void Thread1();
    void Thread2();
private:
    typedef vector<char> BufT;
    volatile BufT buffer_;
    Mutex mtx_; // controls access to buffer_
};

在一个线程的函数中,只是使用LockingPtr<BufT>来得到对于成员变量buffer_的访问控制权。
void SyncBuf::Thread1() {
    LockingPtr<BufT> lpBuf(buffer_, mtx_);
    BufT::iterator i = lpBuf->begin();
    for (; i != lpBuf->end(); ++i) {
        … use *i …
    }
}
这里的代码很容易写也很容易理解–任何时候当你需要使用buff_时,你必须创建一个LockingPtr<BufT>指向它,如果你创建成功,你就可以访问vector的全部接口。
这段代码的优点是如果你有错误,编译器会指出来
void SyncBuf::Thread2() {
    // Error! Cannot access ‘begin’ for a volatile object
    BufT::iterator i = buffer_.begin();
    // Error! Cannot access ‘end’ for a volatile object
    for (; i != lpBuf->end(); ++i) {
        … use *i …
    }
}

2005年06月15日

.NET Internals

Tailor Your Application by Building a Custom Forms Designer with .NET



This article discusses:
  • Design-time environment fundamentals
  • Forms designer architecture
  • The implementation of the forms designer in Visual Studio .NET
  • Services you need to implement to write a forms designer for your own application
This article uses the following technologies:
The .NET Framework and C#


Code download available at:
CustomFormsDesigner.exe (157KB)

or many years, MFC has been a popular framework for building Windows®-based applications. MFC includes a forms designer to make form building, event wiring, and other form-based programming tasks easier. Even though MFC is widely used, it has often been criticized for its shortcomings, most in areas that the Microsoft® .NET Framework addressed. As a matter of fact, the extensible and pluggable design-time architecture of Windows Forms in the .NET Framework has made development a whole lot more flexible than it had been with MFC.

With Windows Forms, for example, you can drag one of your custom controls from the toolbox and drop it onto the Visual Studio® design surface. Amazingly, Windows Forms knows nothing about the control, yet it’s able to host it and let you manipulate its properties. You couldn’t do any of that in MFC.

In this article, I’ll discuss what’s happening under the covers as you design your forms. Then I’ll show you how to build your own bare-bones forms designer that allows your users to create a form in a manner similar to the way they do it using the forms designer in Visual Studio. In order to do that, you’ll need to understand exactly what functionality is provided by the .NET Framework.


Some Windows Forms Basics

Before I get underway with the project, there are several basic concepts that are important to understand. Let’s begin with the definition of a designer. A designer provides the design-mode UI and behavior of a component. For example, when you drop a button on a form, the button’s designer is the entity that determines the appearance and behavior of the button. The design-time environment provides a forms designer and a properties editor to allow you to manipulate components and build user interfaces. The design-time environment also provides services that can be used to interact with, customize, and extend design-time support.

A forms designer provides design-time services and a facility for developers to design forms. The designer host works with the design-time environment to manage designer state, activities (such as transactions), and components. In addition, there are several concepts relating to the components themselves that are important to comprehend. For example, a component is disposable, can be managed by a container, and offers a Site property. It obtains these characteristics by implementing IComponent, as shown here:

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

The IComponent interface is the fundamental contract between the design-time environment and an element to be hosted on a design surface (such as the Visual Studio forms designer). For example, a button can be hosted on the Windows Forms designer because it implements IComponent.

The .NET Framework implements two types of components: visual and nonvisual. A visual component is a user interface element, such as a Control, and a nonvisual component is one without a user interface, such as one that creates a SQL Server database connection. The Visual Studio .NET forms designer distinguishes between visual and nonvisual components when you drag and drop components onto the design surface. Figure 1 shows an example of this distinction.

Figure 1 Visual and Nonvisual Components
Figure 1 Visual and Nonvisual Components

A container contains components and allows contained components to access each other. When a container manages a component, the container is responsible for disposing the component when the container gets disposed—a good idea because a component may use unmanaged resources, which are not automatically disposed of by the garbage collector. Container implements IContainer, which is nothing more than a few methods that let you add and remove components from the container:

public interface IContainer : IDisposable
{

    ComponentCollection Components { get; }
    void Add(IComponent component);
    void Add(IComponent component, string name);
    void Remove(IComponent component);
}

Don’t let the simplicity of the interface fool you. The concept of a container is critical at design time and is useful in other situations as well. For example, you’ve certainly written business logic that instantiates several disposable components. This generally takes the following form:

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

Using a Container object, these lines are reduced to the following:

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();
}

There is more to a container than the automatic disposal of its components. The .NET Framework defines what is called a site, which is related to a container and a component. The relationship between the three is shown in Figure 2. As you can see, a component is managed by exactly one container and each component has exactly one site. When building a forms designer, the same component cannot appear on more than one design surface. However, multiple components can be associated with the same container.

Figure 2 Relationships
Figure 2 Relationships

A component’s lifecycle can be controlled by its container. In return for lifetime management, a component gains access to the services provided by the container. This relationship is analogous to a COM+ component living inside the COM+ container. By allowing the COM+ container to manage it, the COM+ component can participate in transactions and use other services provided by the COM+ container. In a design-time context, the relationship between a component and its container is established through a site. When you place a component on a form, the designer-host creates a site instance for the component and its container. When this relationship is established, the component has been "sited" and uses its ISite property to get to the services provided by its container.


Services and Containers

When a component allows a container to take ownership of it, the component gains access to the services provided by that container. A service, in this context, can be thought of as a function that has a well-known interface, can be obtained from a service provider, is stored in service containers, and is addressable by its type.

Service providers implement IServiceProvider as shown here:

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

Clients obtain a service by supplying to the service provider’s GetService method the type of the service they want. Service containers act as a repository for services and implement IServiceContainer, thus providing a means of adding and removing services. The following code shows the definition of IServiceContainer. Note that the service definition simply contains methods to add and remove a service.

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);
}

Because a service container can store and retrieve services, they are also considered service providers and as such implement IServiceProvider. The combination of services, service providers, and service containers form a simple design pattern that has many advantages. For example, the pattern:

  • Creates loose couplings between the client components and the services they use.
  • Creates a simple service repository and discovery mechanism, which allows an application (or parts of applications) to scale well. You can build your application with exactly the necessary parts, then add additional services later without making any drastic changes to your application or module.
  • Provides the facility to implement lazy loading of services. The AddService method is overloaded to create services the first time they are queried.
  • Can be used as an alternative to static classes.
  • Promotes contract-based programming.
  • Can be used to implement a factory service.
  • Can be used to implement a pluggable architecture. You can use this simple pattern to load plug-ins and provide services to plug-ins (such as logging and configuration).

The design-time infrastructure uses this pattern quite extensively so it’s important to understand it thoroughly.


Let’s Build a Forms Designer

Now that you understand the basic concepts behind the design-time environment, I’ll build upon them by examining the architecture of the forms designer (see Figure 3).

Figure 3 Forms Designer Architecture
Figure 3 Forms Designer Architecture

At the heart of the architecture sits the component. All other entities, directly or indirectly, work with components. The forms designer is the glue that connects the other entities. Forms designers use a designer host to obtain access to the design-time infrastructure. The designer host uses design-time services and provides some of services of its own. Services can, and often do, use other services.

The .NET Framework does not expose the forms designer in Visual Studio .NET because that implementation is application-specific. Even though the actual interfaces are not exposed, the design-time framework is there. All you have to do is provide implementations specific to your forms designer and then submit your version to the design-time environment to use.

My sample forms designer is shown in Figure 4. Like every forms designer, it has a toolbox for users to select tools or controls, a design surface to build forms, and a properties grid to manipulate component properties.

Figure 4 Custom Forms Designer Sample
Figure 4 Custom Forms Designer Sample

First I’ll build the toolbox. Before I do, however, I need to decide how I want to present tools to the user. Visual Studio .NET has a navigation bar with several groups, each of which contains tools. To build the toolbox, you must do the following:

  1. Create a user interface that displays tools to the user
  2. Implement the IToolboxService
  3. Plug the IToolboxService implementation into the design-time environment
  4. Handle events, such as tool selection and drag and drop

For any real-world application, building the toolbox user interface can be quite time consuming. The first design decision you have to make is how to discover and load the tools, and there are several viable approaches. With the first method, you can hardcode the tools to be displayed. This is not recommended unless your application is extremely simple and very little maintenance will be required in the future.

The second method involves reading the tools from a configuration file. For example, the tools could be defined as follows:

<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>

This method has the advantage that you can add or subtract tools and not have to recompile code to alter the tools shown in the toolbox. Additionally, the implementation is fairly simple. You implement a section handler to read the Toolbox section and return a list of ToolboxItems.

The third approach is to create a class for each tool and decorate the class with an attribute that encapsulates things like display name, group, and bitmap. At startup, the application loads a set of assemblies (from a well-known location, as specified in a configuration file, or something similar), and then looks for types with a particular decoration (such as ToolboxAttribute). Types that have this decoration are loaded into the toolbox. This method is probably the most flexible and allows for great tool discovery through reflection, but it also requires a bit more work. In my sample application, I use the second method.

The next important step is to obtain toolbox images. You could spend days trying to create your own toolbox images, but it would be very handy to somehow access the toolbox images in the Visual Studio .NET toolbox. Luckily, there is a way to do that. Internally, the Visual Studio .NET toolbox is loaded using a variation of the third method. This means that the components and controls are decorated with an attribute (ToolboxBitmapAttribute) that defines where to obtain an image for the component or control.

In the sample application, the toolbox contents (groups and items) are defined in the application configuration file. To load the toolbox, a custom section handler reads the Toolbox section and returns a binding class. The binding class is then passed to the LoadToolbox method of the TreeView control that represents the toolbox, as shown in Figure 5.

The LoadItem method creates a ToolboxItem instance for the given type and then calls GetItemImage to get the image associated with that type. The method gets the attribute collection for the type to find the ToolboxBitmapAttribute. If it finds the attribute, it returns the image so it can be associated with the newly created ToolboxItem. Note that the method uses the TypeDescriptor class, a utility class in the System.ComponentModel namespace that is used to obtain attribute and event information for a given type.

Now that you know how to build the toolbox user interface, the next step is to implement IToolboxService. Since this interface is tied directly to the toolbox, it’s handy to just implement this interface in the TreeView-derived class. Most of the implementation is straightforward, but you do need to pay special attention to how you handle drag and drop operations and to how you serialize toolbox items (see the toolboxView_MouseDown method in the ToolboxService implementation in the code download for this article, available from the MSDN®Magazine Web site). The final step in the process is to hook the service implementation into the design-time environment, which I’ll demonstrate how to do after I discuss how to implement the designer host.


Implementing Services

The forms designer infrastructure is built on top of services. There are a set of services that you’re required to implement, and then there are some that simply enhance the functionality of the forms designer if you implement them. This is an important aspect of the services pattern I talked about earlier, as well as of the forms designer. You can start by implementing the base set and then add additional services later.

The designer host is the hook into the design-time environment. The design-time environment uses the host service to create new components when users drag and drop components from the toolbox, to manage designer transactions, to look up services when users manipulate components, and so on. The host service definition, IDesignerHost, defines methods and events. In the host implementation, you provide implementations for the host service along with several other services. These should include IContainer, IComponentChangeService, IExtenderProviderService, ITypeDescriptionFilterService, and IDesignerEventService.


Designer Host

The designer host is the nucleus of the forms designer. When the constructor of the host is called, the host uses the parent service provider (IServiceProvider) to construct its service container. It is very common to chain providers in this manner in order to achieve a trickle-down effect. After the service container is created, the host adds its own services to the provider, as shown in Figure 6.

When a component is dropped onto the design surface, it needs to be added to the host’s container. Adding a new component is a fairly involved operation because you have to perform several checks and fire off some events as well (see Figure 7).

If you overlook the checks and events, you can summarize the addition algorithm as follows. First, a new IComponent is created for the type and a new ISite is created for the component. This establishes the site-to-component association. Note that the site’s constructor accepts a designer host instance. The site constructor takes the designer host and the component so that it can establish the component-container relationship shown in Figure 2. Then the component designer is created, initialized, and added to the component-to-designer dictionary. Finally, the new component is added to the designer host container.

Removing a component requires a bit of cleanup. Again, overlooking simple checks and validation, the remove operation amounts to removing the designer, disposing the designer, removing the component’s site, and then disposing the component.


Designer Transactions

The concept of designer transactions is analogous to database transactions since they both group a sequence of operations in order to treat the group as a unit of work and enable a commit/abort mechanism. Designer transactions are used throughout the design-time infrastructure to support the canceling of operations and to enable views to delay updates of their displays until the entire transaction is completed. The designer host provides the facility to manage designer transactions through the IDesignerHost interface. Managing transactions is not very difficult (see DesignerTransactionImpl.cs in the sample application).

DesignerTransactionImpl represents a single operation in a transaction. When the host is asked to create a transaction, it creates an instance of DesignerTransactionImpl to manage the single change. The host tracks the transactions while instances of DesignerTransactionImpl manage each change. If you don’t implement transaction management, you will get some interesting exceptions while you work with the forms designer.


Interfaces

As I’ve said, components are put into containers for the purpose of lifetime management and to provide them with services. The designer host interface, IDesignerHost, defines methods to create and remove components, so you shouldn’t be surprised if the host offers this service. Again, the container service defines methods to add and remove components and these overlap with the CreateComponent and DestroyComponent methods of IDesignerHost. Therefore, most of the heavy lifting is done in the add and remove methods of the container, and the create and destroy methods simply forward the calls to these methods.

IComponentChangeService defines component change, add, remove, and rename events. It also defines methods for component changed and changing events, which are called by the design-time environment when a component is changing or has changed (such as when a property is changed). This service is offered by the designer host because components get created and destroyed through the host. In addition to creating and destroying components, the host also handles component rename operations through the create method. The rename logic is simple, yet interesting:

// 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;
}

The implementation of this interface is simple enough that you can defer the rest to the sample application.

ISelectionService deals with component selections on the design surface. When users select components, the SetSelectedComponents method is called by the design-time environment with the selected components. The implementation of SetSelectedComponents is shown in Figure 8.

The selection service tracks component selections on the designer surface. Other services, such as IMenuCommandService, use this service when they need to obtain information about selected components. In order to provide this information, the service maintains an internal list that represents the currently selected components. The design-time environment calls SetSelectedComponents with a collection of components when a change has been made to the selection of components. For example, if a user selects one component and then holds down the shift key and selects another three, the method is called for each addition to the selection list. Each time the method is called, the design-time environment tells us which components are affected and how (via the SelectionTypes enumeration). The implementation looks at how the components were changed in order to determine whether components need to be added to or removed from the internal selected list. After modifying the internal selection list, I fire the Selection Changed event (see method selectionService_SelectionChanged in SelectionServiceImpl.cs) so that the properties grid can be updated with the new selections. The application’s main form, MainWindow, subscribes to the selection service’s selection changed events in order to update the properties grid with the selected component(s).

Also note that the selection service defines a PrimarySelection property. The primary selection is always set to the last item selected. I’ll use this property in my discussion of IMenuCommandService when I talk about showing the correct designer context menu.

The selection service is one of the more difficult services to implement correctly because it has some valuable features that complicate the implementation. For example, in a real-world application it makes sense to handle keyboard events, such as Ctrl+A, and also manage issues around handling a large selection list.

The ISite implementation is one of the more important implementations and is shown in Figure 9.

You’ll notice that the SiteImpl also implements IDictionaryService, which is a bit unusual because all of the other services I implemented were tied to the designer host. It turns out that the design-time environment requires you to implement IDictionaryService for every sited component. The design-time environment uses the IDictionaryService on every site to maintain a data table that is used throughout the designer framework. Another thing to note about the site implementation is that since ISite extends IServiceProvider, the class provides an implementation for GetService. The designer framework calls this method when it’s looking for a service implementation on a site. If the service request is for IDictionaryService, the implementation simply returns itself, the SiteImpl. For all other services, the request is forwarded to the site’s container (for example, the host).

Every component has to have a unique name. As you drag and drop components from the toolbox onto the design surface, the design-time environment uses an implementation of INameCreationService to generate each component’s name. The component’s name is the Name property that is displayed in the properties window when the component is selected. The definition of the INameCreationService interface is shown here:

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

In the sample application, the CreateName implementation uses the container and dataType to calculate a new name. In a nutshell, the method counts the number of components whose type is equivalent to dataType and then uses that count with the dataType to come up with a unique name.

The services discussed so far have all dealt with components, either directly or indirectly. The menu command service, on the other hand, is specific to designers. It is responsible for tracking menu commands and designer verbs (actions), and showing the correct context menu when a user chooses a particular designer.

The menu command service handles the tasks of adding, removing, finding, and executing menu commands. In addition, it defines methods to track designer verbs and to show designer context menus for designers that support them. The core of this implementation lies in showing the correct context menu. As such, I’ll defer what little implementation is left to the sample application and instead focus on how to show context menus.


Tracking Designer Verbs and Showing Context Menus

There are two types of designer verbs: global and local. Global verbs exist for all designers and local verbs are specific to each designer. An example of a local verb can be seen when you right-click a tab control on the design surface (see Figure 10).

Figure 10 The Design Surface
Figure 10 The Design Surface

Right-clicking a tab control adds local verbs that allow you to add and remove tabs on the control. An example of a global verb can be seen in the Visual Studio forms designer when you right-click anywhere on the design surface. Regardless of where and on what object you click, you always see two menu items: View Code and Properties. Each designer has a Verbs property that contains the verbs representing functionality specific to that designer. For the tab control designer, for example, the verbs collection contains two members: Add Tab and Remove Tab.

When a user right-clicks a tab control on the design surface, the design-time environment calls the ShowContextMenu method on the IMenuCommandService (see Figure 11).

This method is responsible for showing the context menu for the selected object’s designer. As you can see in Figure 11, the method gets the selected component from the selection service, obtains its designer from the host, gets the verbs collection from the designer, and then adds a menu item to the context menu for each verb. After the verbs have been added, the context menu is shown. Note that when you create new menu items for designer verbs, you also attach a click handler for the menu item. The custom click handler handles clicking events for all menu items (see the method MenuItemClickHandler in the sample application).

When a user selects a menu item from a designer context menu, the custom handler is called to execute the verb associated with the menu item. In the handler, you retrieve the verb associated with the menu item and invoke it.


ITypeDescriptorFilterService

I mentioned earlier that the TypeDescriptor class is a utility class that is used to obtain information about a type’s properties, attributes, and events. The ITypeDescriptorFilterService can filter this information for sited components. The TypeDescriptor class uses ITypeDescriptorFilterService when it attempts to return a sited component’s properties, attributes, and/or events. Designers who want to modify the metadata available to the design-time environment for the component they are designing can do so by implementing IDesignerFilter. The ITypeDescriptorFilterService defines three methods that allow designer filters to hook into and modify a sited component’s metadata. Implementing ITypeDescriptorFilterService is simple and intuitive (see TypeDescriptorFilterService.cs in the sample application).


Putting It All Together

If you’ve taken a look at the sample application and run the forms designer, you may be wondering how all of the services have come together. You can’t build the forms designer incrementally—that is, you can’t implement one service, test the application, and then write another service. You have to implement all of the required services, build the user interface, and tie them all together before you can test the application. That’s the bad news. The good news is that I have done most of the work in the services I have implemented. All that’s left to do is a bit of crafting.

To start, take a look at the CreateComponent method of the designer host. When creating a new component, it’s important to see if it’s the first component (if rootComponent is null). If it is the first component, you have to create a specialized designer for the component. The specialized base designer is an IRootDesigner because the top most designer in the designer hierarchy has to be an IRootDesigner (see Figure 12).

Now that you know that the first component has to be the root component, how do you ensure that the right component is the first? The answer is that the design surface ends up being the first component because you create this control, as a Form, during the main window initialization routine (see Figure 13).

Handling the root component is the only tricky part of the adhesive between the designer host, the design-time environment, and the user interface. The rest is easily understood by spending a bit of time reading the code.


Debugging the Project

Implementing a forms designer is not a trivial exercise. There is little existing documentation on the subject. And once you figure out where to start and what services to implement, debugging the project is going to be painful because you have to implement a set of required services and plug them in before you can start to debug any of it. Finally, once you implement the required services, the error messages that you get are not very helpful. For example, you may get a NullReferenceException on a line that calls the internal design-time assemblies, which you can’t debug, so you’re left wondering which service failed where.

Additionally, because the design-time infrastructure is built on top of the service pattern I discussed earlier, debugging services can be a problem. A technique to mitigate debugging pains is to log service requests. Logging which service request was queried, whether it passed or failed, and where it was called from (taking advantage of Environment.StackTrace) within the framework can be a very useful debugging tool to add to your arsenal.


Conclusion

I’ve provided an overview of the base services that you need to implement in order to get your forms designer up and running. In addition, you’ve seen how to configure the toolbox based on the needs of your application by changing the configuration file. What remains is to tweak the existing services and to implement a few others depending on your needs.

2005年04月21日
Log4j基本使用方法

作者:佚名     来自:未知

  Log4j由三个重要的组件构成:日志信息的优先级,日志信息的输出目的地,日志信息的输出格式。日志信息的优先级从高到低有ERROR、WARN、INFO、DEBUG,分别用来指定这条日志信息的重要程度;日志信息的输出目的地指定了日志将打印到控制台还是文件中;而输出格式则控制了日志信息的显示内容。

  一、定义配置文件

  其实您也可以完全不使用配置文件,而是在代码中配置Log4j环境。但是,使用配置文件将使您的应用程序更加灵活。Log4j支持两种配置文件格式,一种是XML格式的文件,一种是Java特性文件(键=值)。下面我们介绍使用Java特性文件做为配置文件的方法:

  1.配置根Logger,其语法为:

  log4j.rootLogger = [ level ] , appenderName, appenderName, …

  其中,level 是日志记录的优先级,分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者您定义的级别。Log4j建议只使用四个级别,优先级从高到低分别是ERROR、WARN、INFO、DEBUG。通过在这里定义的级别,您可以控制到应用程序中相应级别的日志信息的开关。比如在这里定义了INFO级别,则应用程序中所有DEBUG级别的日志信息将不被打印出来。 appenderName就是指定日志信息输出到哪个地方。您可以同时指定多个输出目的地。

  2.配置日志信息输出目的地Appender,其语法为:

  log4j.appender.appenderName = fully.qualified.name.of.appender.class
  log4j.appender.appenderName.option1 = value1
  …
  log4j.appender.appenderName.option = valueN

  其中,Log4j提供的appender有以下几种:
  org.apache.log4j.ConsoleAppender(控制台),
  org.apache.log4j.FileAppender(文件),
  org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件),
  org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件),
  org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

  3.配置日志信息的格式(布局),其语法为:

  log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class
  log4j.appender.appenderName.layout.option1 = value1
  …
  log4j.appender.appenderName.layout.option = valueN

  其中,Log4j提供的layout有以下几种:
  org.apache.log4j.HTMLLayout(以HTML表格形式布局),
  org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
  org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
  org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)

  Log4J采用类似C语言中的printf函数的打印格式格式化日志信息,打印参数如下: %m 输出代码中指定的消息

  %p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL
  %r 输出自应用启动到输出该log信息耗费的毫秒数
  %c 输出所属的类目,通常就是所在类的全名
  %t 输出产生该日志事件的线程名
  %n 输出一个回车换行符,Windows平台为“\r\n”,Unix平台为“\n”
  %d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
  %l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)

  二、在代码中使用Log4j

  1.得到记录器

  使用Log4j,第一步就是获取日志记录器,这个记录器将负责控制日志信息。其语法为:

  public static Logger getLogger( String name)

  通过指定的名字获得记录器,如果必要的话,则为这个名字创建一个新的记录器。Name一般取本类的名字,比如:

  static Logger logger = Logger.getLogger ( ServerWithLog4j.class.getName () )

  2.读取配置文件

  当获得了日志记录器之后,第二步将配置Log4j环境,其语法为:

  BasicConfigurator.configure (): 自动快速地使用缺省Log4j环境。
  PropertyConfigurator.configure ( String configFilename) :读取使用Java的特性文件编写的配置文件。
  DOMConfigurator.configure ( String filename ) :读取XML形式的配置文件。

  3.插入记录信息(格式化日志信息)

  当上两个必要步骤执行完毕,您就可以轻松地使用不同优先级别的日志记录语句插入到您想记录日志的任何地方,其语法如下:

  Logger.debug ( Object message ) ;
  Logger.info ( Object message ) ;
  Logger.warn ( Object message ) ;
  Logger.error ( Object message ) ;