All of the BindingElement objects shown in the preceding code example derive from the System.ServiceModel.Channels.BindingElement abstract type. A BindingElement is a factory object. More specifically, the BindingElement type defines methods that return a channel factory or a channel listener. A BindingElement object is seldom used in isolation. A BindingElement usually resides in a BindingElement collection, and the primary way to create a BindingElement is via the Binding.CreateBindingElements method. As with channel factories, channel listeners, and channels, there is no one-size-fits-all BindingElement. As you saw in the preceding code example, the WCF type system abounds with types derived from BindingElement, and each represents some discrete part of the messaging capability supported by WCF out of the box. Developers are free to build their own types derived from the BindingElement type, however. In keeping with the WCF programming model, custom BindingElement-derived types are necessary any time you build a custom channel, channel factory, or channel listener.
Like the Binding type, the BindingElement type hierarchy is very simple. It implements no interfaces and derives directly from Object. The BindingElement type is shown here:
public abstract class BindingElement { // default constructor protected BindingElement() { ... } // clones the BindingElement argument protected BindingElement(BindingElement elementToBeCloned) { ... } // Factory method for channel factories public virtual IChannelFactory<TChannel> BuildChannelFactory<TChannel>( BindingContext context) { ... } // Factory method for channel listeners public virtual IChannelListener<TChannel> BuildChannelListener<TChannel>( BindingContext context) where TChannel: class, IChannel { ... } // Test methods for channel factories and listeners public virtual bool CanBuildChannelFactory<TChannel>( BindingContext context) { ... } public virtual bool CanBuildChannelListener<TChannel>( BindingContext context) where TChannel: class, IChannel { ... } // returns a cloned BindingElement public abstract BindingElement Clone(){ ... } // Query mechanism public abstract T GetProperty<T>(BindingContext context) where T: class { ... } }
As odd as this might sound, the constructors of the BindingElement type are closely tied to the Clone method. Let’s look at the constructors first. Neither has any implementation; they simply return. The purpose of the constructor that has an argument of type BindingElement is to allow derived types to clone themselves. Derived types are likely to have some state associated with them, and their form of this constructor should retrieve the values for these fields and assign them to the new object. The BindingElement type also defines an abstract method named Clone. As its name implies, this method returns a new instance of a BindingElement. The state of the BindingElement returned from the Clone method must be exactly the same as the instance that the Clone method was called on. Because a BindingElement-derived type can itself be used as a base type for another BindingElement, the Clone method should call the protected constructor in that type. This approach also ensures that derived types will survive the addition of a field to the BindingElement type in the future. The following code snippet shows the proper use of the BindingElement constructor and the Clone method:
public class SomeBindingElement : BindingElement { private String someValue; // an example field public SomeBindingElement(){ this.someValue = "SomeString"; } protected SomeBindingElement (SomeBindingElement elementToBeCloned) : base(elementToBeCloned) { // set the new object's field to the value of the arg this.someValue = elementToBeCloned.someValue; } // clone method calls the protected ctor public override BindingElement Clone(){ return new SomeBindingElement(this); } // other implementation omitted for clarity } public sealed class OtherBindingElement : SomeBindingElement { private String otherValue; public OtherBindingElement(){ this.otherValue = "SomeString"; } private OtherBindingElement(OtherBindingElement elementToBeCloned) : base(elementToBeCloned) { // set the new object's field to the value of the arg // base .ctor gets called also this.otherValue = elementToBeCloned.otherValue; } // clone method calls the protected ctor public override BindingElement Clone(){ return new OtherBindingElement(this); } // other implementation omitted for clarity }
The Clone method is vital when testing the capabilities of a Binding, as well as when building the channel factory and channel listener stacks. Nodes in a BindingElement collection are consumed when testing the capabilities of a Binding as well as during the construction of channel factory and channel listener stacks. The BindingElement collection consumed during these procedures is not the same object returned from the CreateBindingElements method, but rather a clone of that object. Since cloning a collection is a matter of cloning each item in the collection, cloning a BindingElement collection is a matter of cloning each BindingElement. You’ll learn more about the Clone method on the BindingElement type in the section “The BindingContext Type” later in this chapter.
The BindingElement type also defines two test methods named CanBuildChannelFactory<TChannel> and CanBuildChannelListener<TChannel> that return a Boolean indicating whether it is possible to build a channel factory stack or channel listener stack associated with a TChannel channel shape. Remember that BindingElement objects seldom exist in isolation, but rather they exist as part of a BindingElement collection. This is important when considering whether a BindingElement can create a channel factory stack or channel listener stack associated with a channel shape. Consider the case of a BindingElement collection that consists of a BinaryMessageEncodingBindingElement and an HttpTransportBindingElement. In this case, the test methods on the BinaryMessageEncodingBindingElement should return true only when the TChannel generic parameter is a channel shape compatible with the request/reply Message Exchange Pattern (MEP). If, however, we consider the case of a BindingElement collection that consists of a BinaryMessageEncodingBindingElement and a TcpTransportBindingElement, the test methods on the BinaryMessageEncodingBindingElement will not return true when the TChannel generic parameter is compatible with the request/reply MEP. The contributing factor for the BinaryMessageEncodingBindingElement is the other BindingElement in the collection. To generalize a bit, the test methods on a BindingElement object depend on the BindingElement objects that reside lower in the BindingElement collection.
In channel stacks, channel factory stacks, and channel listener stacks, each node in the stack has a reference to the next node in the stack. With BindingElement collections, however, an individual BindingElement has no reference to other BindingElement objects in the BindingElement collection. This certainly presents a problem in the test methods, because an individual BindingElement object needs to test lower BindingElement objects before returning a value. The answer to this riddle lies in the argument to the test methods in the BindingElement type.
The test methods on the BindingElement type resemble the test methods defined in the Binding type. They are different, however, in their arguments. In the Binding type, the arguments to these methods are a BindingParameterCollection or a param, which is an array of type Object. On the BindingElement type, however, the test methods have an argument of type BindingContext. You’ll learn about the BindingContext type in more detail in the section “The BindingContext Type” later in this chapter, but we must examine some aspects of the BindingContext type here to fully understand how these test methods on the BindingElement type work. A BindingContext object stores an expendable list of BindingElement objects (a cloned version of the one returned from Binding.CreateBindingElements), a BindingParameterCollection, and some properties related to the listening address. The important point here is that a BindingContext object contains a consumable list of BindingElement objects, and that consumable list serves as a way for BindingElement objects to interrogate BindingElement objects that reside lower in the list.
With this in mind, the implementation of a test method on a BindingElement-derived type could look like the following:
public override Boolean CanBuildChannelListener<TChannel>( BindingContext context) { if (context == null) { throw new ArgumentNullException("context"); } // This BindingElement works only with the datagram MEP if (typeof(TChannel) == typeof(IInputChannel)) { // check if the other elements work with the datagram MEP return context.CanBuildInnerChannelListener<IInputChannel>(); } // if not, return false return false; } public override Boolean CanBuildChannelFactory<TChannel>( BindingContext context) { if (context == null) { throw new ArgumentNullException("context"); } // This BindingElement works only with the datagram MEP if (typeof(TChannel) == typeof(IOutputChannel)) { // check if the other elements work with the datagram MEP return context.CanBuildInnerChannelFactory<IOutputChannel>(); } return false; }
Notice that both test methods leverage instance methods on the BindingContext argument. As you’ll see in the section “The BindingContext Type” later in this chapter, the CanBuildInnerChannelFactory<TChannel> and CanBuildInnerChannelListener<TChannel> methods on the BindingContext type walk the remaining BindingElement objects and invoke the test methods on those BindingElement objects.
The query mechanism in the BindingElement type appears similar to the one you saw in channels, channel factories, and channel listeners. Structurally, querying a BindingElement object for capabilities is similar to the test methods shown in the preceding section, because a BindingElement that cannot directly return a value must be able to delegate the query to another BindingElement. As you saw in the preceding section, the test methods on a BindingElement rely on the BindingContext type to provide references to the other BindingElement objects in the BindingElement collection. In a similar fashion, the query mechanism in the BindingElement type relies on the BindingContext type to delegate queries to other BindingElement objects in the collection. The following is an implementation of the query mechanism in a BindingElement-derived type that shows how to delegate queries to the BindingContext argument:
public override T GetProperty<T>(BindingContext context) { if (context == null) { throw new ArgumentNullException("context"); } // this BindingElement delegates all queries except for // SomeCapability queries to other BindingElements if (typeof(T) != typeof(SomeCapabilility)) { // delegate the query to other BindingElements // via the BindingContext return context.GetInnerProperty<T>(); } // return the capability – in this case it // is a field in the BindingElement return (T) this.someCapability; }
In this example, the SomeCapability type is obviously fictional, but it represents any capability query that a BindingElement can return. The GetInnerProperty<T> method on the context type finds the next BindingElement in the list and invokes the GetProperty<T> method on that BindingElement. It’s important to note that the BindingContext argument should be used only if the capability is not known to the current BindingElement (as shown in this example).
The two most important methods defined in the BindingElement type are the BuildChannelFactory<TChannel> and BuildChannelListener<TChannel> methods. I assert that these methods are the most important methods in the BindingElement type because they are the factory methods that create a channel factory or a channel listener, respectively. The channels created by the returned channel factory or channel listener are compatible with the TChannel generic parameter. Both the BuildChannelFactory<TChannel> and BuildChannelListener<TChannel> methods have an argument of type BindingContext. Like the test methods and the query mechanism, the BindingContext argument in these factory methods allows an entire channel factory stack or channel listener stack to be built from a single call site. The implementation of these BindingElement methods is roughly as follows:
public virtual IChannelFactory<TChannel> BuildChannelFactory<TChannel>( BindingContext context) { if (context == null) { throw new ArgumentNullException("context"); } // delegate the call to the context argument return context.BuildInnerChannelFactory<TChannel>(); } public virtual IChannelListener<TChannel> BuildChannelListener<TChannel>( BindingContext context) where TChannel: class, IChannel { if (context == null) { throw new ArgumentNullException("context"); } // delegate the call to the context argument return context.BuildInnerChannelListener<TChannel>(); }
In BindingElement-derived types, these factory methods also need to return the channel factory stack or channel listener stack that contains the channel factory or channel listener that the BindingElement is associated with. Recalling the DelegatorChannelListener and the DelegatorChannelFactory example types from Chapter 7, a BindingElement associated with these types could look like the following:
// the type should be public, since it is // part of the developer-facing API public sealed class DelegatorBindingElement : BindingElement { // The factory method for the channel factory stack public override IChannelFactory<TShape> BuildChannelFactory<TShape>( BindingContext context) { if (context == null) { throw new ArgumentNullException("context"); } // call the test method to ensure that TShape will work if (!this.CanBuildChannelFactory<TShape>(context)) { throw new InvalidOperationException("Unsupported channel type"); } // instantiate a new DelegatorChannelFactory, // passing the context as an argument DelegatorChannelFactory<TShape> factory = new DelegatorChannelFactory<TShape>(context); // cast to an IChannelFactory<TShape> and return return (IChannelFactory<TShape>)factory; } // the factory method for the channel listener stack public override IChannelListener<TShape> BuildChannelListener<TShape>( BindingContext context) { if (context == null) { throw new ArgumentNullException("context"); } // call the test method to ensure that TShape will work if (!this.CanBuildChannelListener<TShape>(context)) { throw new InvalidOperationException("Unsupported channel type"); } // instantiate a new DelegatorChannelListener, // passing the context as an argument DelegatorChannelListener<TShape> listener = new DelegatorChannelListener<TShape>(context); // cast to an IChannelListener<TShape> and return return (IChannelListener<TShape>)listener; } // other implementation omitted for clarity }
As with the test methods and the query mechanism, the real work in the factory methods is done by the BindingContext argument. It is important to note that the constructors of the channel listener and channel factory both accept arguments of type BindingContext. Many channel listeners and channel factories also accept an argument that is of type BindingElement, or some type derived from BindingElement. This is a means by which the channel factory or channel listener can receive information from the BindingElement. Notice also that the factory methods in the preceding example cast the channel factory stack or channel listener stack to the interface before returning.