Now that the first hurdle has been overcome, it is time to enable proper rendering of the control inside the design environment. After finishing the rendering designer, it can render either one of the templates, enabling the look which is displayed in the following image. There is only one catch. The user isn’t able to specify which template to render. The designer will always render the first template. This will be remedied using smart-tags in the next part.
It would be a shame not to build upon the code presented in the previous chapter. Therefore, the rendering logic will be placed in a new class which derives from the previous non-rendering designer.
|
Figure 1. The rendering designer used on the DuoView control |
Storing design-time settings
The DuoView control exposes the ActiveDisplayMode property which is used to choose the right template to render. To allow for design-time changes in the displayed template, the designer will need to change the value of this property. You should be extra careful when changing the state of the control on the designer surface. The control instance will be serialized into dotNET code, which persists the changes made from inside the designer.
It is possible that a user of the DuoView control wishes to display the first template at runtime, and wants to design the second. The designer would still need to change the ActiveDisplayMode of the DuoView to display the second template at design-time. However this change should not be serialized into markup. When changing the ActiveDisplayMode on the DuoView on the designer surface, obtained from the Component property, the change will get serialized into markup. This integrates the runtime and design-time template, a situation which is not desired.
The designer framework provides the ability to create a temporary copy of the control on the designer surface. By setting the UsePreviewControl property, that copy can be obtained using the ViewControl property. This copied control can be used to render at design-time. When the ActiveDisplayMode is set on the copy, it will not be serialized into markup because it is thrown away after the rendering is complete.
Storing the design-time display mode using DesignerState
Apart from the possibility to ask the DuoView to display a certain template before rendering, the user must also be able to switch the template. The designer exposes a property which the user can change using the smart-tag implemented in the next chapter. The value of this property can be stored in one of two locations, not counting exoteric solutions as a database. It will either be a field in the designer class, or stored using DesignerState. When the user switches between the design and markup view, designers are re-instantiated. If the property is stored in a field, the field will be reset to its default value. The DesignerState container can be used to persist settings across designer sessions. The container uses an implementation of the IComponentDesignerStateService to store the values.
protected DuoView.DisplayMode DesignTimeDisplayMode
{ get
{ DuoView.DisplayMode displayMode =
DuoView.DisplayMode.FirstTemplate;
object view = base.DesignerState["DesignTimeDisplayMode"];
if (view != null)
{ displayMode = (DuoView.DisplayMode)view;
}
return displayMode;
}
set
{ if (!Enum.IsDefined(typeof(DuoView.DisplayMode), value))
{ throw new ArgumentOutOfRangeException();
}
base.DesignerState["DesignTimeDisplayMode"] = value;
}
}
Figure 2. Implementing design-time state using the DesignerState container
The implementation is easy. The only caveat is when the user hasn’t switched the display mode yet. The DesignerState container will not contain any values, which could result in a cast of NULL into a value type, a big no-no.
Now that the right template to render at design-time can be indicated, the setting can be used to access the actual template on the copied instance of the DuoView control. The CurrentViewControlTemplate property will be used to access this information.
protected ITemplate CurrentViewControlTemplate
{ get
{ ITemplate current = null;
switch (DesignTimeDisplayMode)
{ case DuoView.DisplayMode.FirstTemplate:
current = ((DuoView)ViewControl).FirstTemplate;
break;
case DuoView.DisplayMode.SecondTemplate:
current = ((DuoView)ViewControl).SecondTemplate;
break;
}
return current;
}
}
Figure 3. Accessing the right template on a temporary copy of the DuoView
Rendering
So far the plumbing, all that is required now is some code which renders the control on the designer surface. The ControlDesigner base class provides the GetDesignTimeHtml method which will be called when the control needs to be rendered. Using an override the rendering logic can be customized.
public override string GetDesignTimeHtml()
{ string html = String.Empty;
if (this.CurrentViewControlTemplate != null)
{ DuoView module = (DuoView)ViewControl;
IDictionary designerState = new HybridDictionary(1);
designerState["CurrentView"] = this.DesignTimeDisplayMode;
((IControlDesignerAccessor)module).SetDesignModeState(
designerState);
module.DataBind();
html = base.GetDesignTimeHtml();
}
return html;
}
Figure 4. The code for rendering the control during design-time
A control renders itself using various Render… methods. This is also the case during design-time. The base implementation of GetDesignTimeHtml will call the RenderControl method on the control found using the ViewControl property.
The DuoView needs to be told which template to display before the base implementation can start rendering. The Control base class of the DuoView control provides a method which can be used by a designer to communicate with the control under design. The IControlDesignerAccessor interface provides the necessary contract between the designer and the control. This interface is used to send a message to the DuoView ordering it to display the right template. The DataBind method needs to be called before rendering. Not doing so will cause controls inside the template such as a BulletedList to render incorrectly.
The last bit required for it all to work correctly is not added to the designer class, but to the DuoView control. The control needs to override the SetDesignModeState method in order for it to receive the messages sent by the designer.
protected override void SetDesignModeState(IDictionary data)
{ if (data != null)
{ object currentView = data["CurrentView"];
if (currentView != null)
{ ActiveDisplayMode = (DisplayMode)currentView;
ChildControlsCreated = false;
}
}
}
Figure 5. Communicating with the control on the designer surface (DuoView class)
That is all that is required to support rendering during design-time. Using just four small code blocks, of which two are just plumbing, it was a simple task to add the rendering logic to the designer. The best thing is all the code is reusable for the next addition to the designer, smart-tags. Only the GetDesignTimeHtml method will be extended a little bit to provide a better feel for the control.