So I'm on my second paid Atlas gig at the moment and find myself doing purely custom control development while my colleague does all of the page development. He has been very pleased with my approach, so I thought I would share it for others to consume & comment on. As I said, this is my second project, and thus the second iteration of my Atlas framework.
My Atlas Framework
In a nutshell, I create controls that completely replace all of the ASP.Net controls. These controls are generally not extended, but are plumbed with code to enable them to consume Atlas markup (behaviors, bindings, events, etc.). This enables the consumer to code for client side functionality through the control markup. For example, if I wanted to change the visibility of a TextBox when a Button was clicked, I would write it up like so:
<mamanze:Button id="foo" runat="server" text="Click Me!">
<ClientClick>
<mamanze:SetVisibility targetId="bar" VisibilityMode="Visible" />
</ClientClick>
</mamanze:Button>
<mamanze:TextBox id="bar" runat="server" />This is, of course, a very simple example. A couple things set it apart from the way I've seen Atlas done before:
First, my button control is just like an ASP.Net button, but exposes client side functionality as properties. In the case of the Button, it exposes a ClientClick property which is a server side equivalent of an Atlas event, taking a handler as an attribute and a set of actions as child elements.
Second, there is no XML-Script in sight. Personally, I find the concept of XML-Script very cool, but very ugly to develop against. Thus, I wrap it up in pretty, type safe, intellisense rich, declarative markup.
Third, SetVisibility, while a simple operation, is nicely encapsulated for easy reuse. I could just as easily have done this with a handler like:
<mamanze:Button id="foo" runat="server">
<ClientClick handler="$('bar').style.display='none'" />
</mamanze:Button>But the first is clearer to the ASP.Net developer and simpler to modify the functionality. For example, if they wanted to toggle the visibility when the button was clicked instead of just making it visible, with the control all that is needed is to change VisibilityMode to "Toggle". Another nice feature of doing this with control markup is that the id "bar" will, when rendered in the XML-Script, be the ClientID of the control. The SetVisibility action is able to resolve "bar" to the TextBox on the server, and from there output the ClientID.
Yes, I admit it, I finally agree with ScottGu that declarative code is nicer in cases like this than programmatic code.
Markup, not XML-Script
As I said, my control outputs XML-Script. If you were to see the rendered output of the above it would look something like this:
<input type="button" id="foo" value="Click Me!" />
<input type="text" id="bar" />
<script type="text/xml-script">
<page>
<components>
<mamanze:button id="foo">
<click>
<mamanze:setVisibility target="bar" visibilityMode="visible" />
</click>
</mamanze:button>
</components>
</page>
</script>But as a consumer of my controls, the developer does not need to worry about the XML-Script because the controls take care of everything on that front.
Plumbing
This methodology is by no means out of the box. I have a few base classes to support things like Actions, Bindings and Conditions on both the client and server. However, the main class that enables all of this love is the one that handles the XML-Script generation called AtlasHelper. Basically each of the controls I extend or create instantiates an AtlasHelper instance and passes all the Atlas stuff over to the helper.
In essence, the AtlasHelper class looks like this:
public class AtlasHelper
{
public AtlasHelper(IAtlasHelperTarget target, string @namespace, string tagName);
public Dictionary<string, ActionList> EventActions;
public Dictionary<string, string> EventHandlers;
public BindingList Bindings;
public ConditionList Conditions;
public List<IScriptBehavior> Behaviors;
public void Initialize(Page page);
public void RenderBehaviors(ScriptTextWriter writer);
public void RenderScript(ScriptTextWriter writer)
{
writer.WriteStartElement(this.@namespace + ":" + this.tagName);
{
writer.WriteAttributeString("id", this.target.ClientID);
this.target.RenderScriptAttributes(writer);
this.RenderEventHandlers(writer);
this.RenderEventActions(writer);
this.RenderBehaviors(writer);
this.Conditions.RenderList(writer);
this.Bindings.RenderList(writer);
this.target.RenderScriptContent(writer);
}
writer.WriteEndElement();
}
private void RenderEventHandlers(ScriptTextWriter writer);
private void RenderEventActions(ScriptTextWriter writer);
public static Control ResolveControl(Control parent, string id);
}Then every class that needs to render XML-Script simply invokes the AtlasHelper.RenderScript method and implements the IAtlasHelperTarget interface:
public interface IAtlasHelperTarget : IScriptControl
{
void Initialize(Page page);
void RenderScriptAttributes(ScriptTextWriter writer);
void RenderScriptContent(ScriptTextWriter writer);
string ClientID { get; }
IAtlasHelperTarget ScriptParent { get; set; }
}With the above helper and a simple ActionBase class, I can quickly write an Action like the SetVisibilityAction used above like so:
public class SetVisibilityAction : ActionBase
{
public SetVisibilityAction() : base("setVisibilityAction") {}
public VisibilityMode Mode { get; set; }
public string TargetId { get; set; }
public override void Initialize(Page page, Control parent)
{
base.Initialize(page, parent);
ScriptManager scriptManager = ScriptManager.GetCurrent(page);
if (scriptManager != null)
scriptManager.RegisterScriptReference("Mamanze.Actions.SetVisibilityAction.js");
Control target = AtlasHelper.ResolveControl(this.ScriptParent, this.targetId);
if (target != null)
this.targetId = target.ClientID;
}
public override void RenderScriptAttributes(ScriptTextWriter writer)
{
base.RenderScriptAttributes(writer);
if (!string.IsNullOrEmpty(this.TargetId))
writer.WriteAttributeString("targetId", this.TargetId);
if (this.Mode != VisibilityMode.Toggle)
writer.WriteAttributeString("mode", this.Mode.ToString());
}
}Pretty quick, I turned that one out in a few minutes (including a client side matching class). All in all, I've found this technique of creating server side classes for everything and coding directly for Atlas very clean.