Andy isn't the only person inside Conchango working with Atlas at the moment, both Simon and I are evaluating into the framework for our current projects – since Mix06 there has been a lot of buzz about Web 2.0 – and we are trying to see how we can deliver richer user experiences using the Atlas framework.
I've spent the last two weeks delving into the guts of Atlas and the Atlas Control Toolkit – within the first few hours of exploration I hit an issue – I wanted to DataBind a series of CheckBoxes to a “Master” CheckBox which would act as a “Select All” switch:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register Assembly="Microsoft.Web.Atlas" Namespace="Microsoft.Web.UI" TagPrefix="atlas" %>
<%@ Register Assembly="AtlasControlToolkit" Namespace="AtlasControlToolkit" TagPrefix="atlasToolkit" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>ToggleButton "Select All"</title>
<atlas:ScriptManager ID = "ScriptManager1" EnablePartialRendering = "true" runat = "Server"></atlas:ScriptManager>
</head>
<body>
<form id="form1" runat="server">
<div style="width:100%;">
<div style="width:200px;float:right;font-family:verdana;font-size:11px;">
<asp:CheckBox ID="master" runat="server" Text=" Select All" />
<br />
<asp:CheckBox ID="slave1" runat="server" />
<br />
<asp:CheckBox ID="slave2" runat="server" />
<br />
<asp:CheckBox ID="slave3" runat="server" />
<br />
</div>
<br />
<script type="text/xml-script">
<page xmlns:script="http://schemas.microsoft.com/xml-script/2005">
<components>
<checkBox id="master"/>
<checkBox id="slave1">
<bindings>
<binding dataContext="master" dataPath="checked" property="checked" />
</bindings>
</checkBox>
<checkBox id="slave2">
<bindings>
<binding dataContext="master" dataPath="checked" property="checked" />
</bindings>
</checkBox>
<checkBox id="slave3">
<bindings>
<binding dataContext="master" dataPath="checked" property="checked" />
</bindings>
</checkBox>
</components>
</page>
</script>
</div>
</form>
</body>
</html>
This worked perfectly – I was quite stunned at how well client side databinding works and that a simple declarative statement negated the need for lots of DOM parsing via JavaScript. Next, I wanted to add ToggleButton Extenders to prettify the checkboxes:
<atlasToolkit:ToggleButtonExtender ID="ToggleButtonExtender1" runat="server">
<atlasToolkit:ToggleButtonProperties TargetControlID="master" CheckedImageUrl = "ToggleButton_Checked.gif" UncheckedImageUrl = "ToggleButton_Unchecked.gif" ImageHeight="15" ImageWidth="15" />
<atlasToolkit:ToggleButtonProperties TargetControlID="slave1" CheckedImageUrl = "ToggleButton_Checked.gif" UncheckedImageUrl = "ToggleButton_Unchecked.gif" ImageHeight="15" ImageWidth="15" />
<atlasToolkit:ToggleButtonProperties TargetControlID="slave2" CheckedImageUrl = "ToggleButton_Checked.gif" UncheckedImageUrl = "ToggleButton_Unchecked.gif" ImageHeight="15" ImageWidth="15" />
</atlasToolkit:ToggleButtonExtender>
Now the first error occurred – “Assertion Failed: Duplicate ID”. This alert is a known bug which will be fixed in the next CTP release. I discovered that you can suppress these assertions by turning off debug mode in web.config by changing the following line:
<compilation debug="false">
which is not exactly ideal as this means you can no longer debug server side code - no breakpoints will be hit. Once this error had gone I found that the ToggleButton Extender wasn’t working as I would have expected:
i.e. to go from this state:
to this:
Instead it behaved like this:
After a little bit of CSI I discovered that the ToggleButton Extender listens for “propertyChanged” events from its targeted control and only responds to the “checked” property change. After a bit more digging it seems that when you DataBind one CheckBox to another, the “checked” “propertyChanged” event is not raised on the bound controls when they change state. I was a bit perplexed by this and posted a thread to the ASP.NET forum – you can follow the thread here. I came up with a work around that added a onClick event to the underlying checkbox – this made the ToggleButtons work as expected – but it was a definite hack.
I updated the forum thread with my progress and then got a reply stating that “Attaching XML-Script to server controls is not supported”. I had to re-read that message several times before it sank in. The message continued “To extend a server control with behavior, you should build an extender. Here, the behavior is entirely undetermined because attaching the extender will create a control, and the XML-Script is creating another on the same control. As there can be only one control per element, this won't run.” I was pretty flabbergasted by this post. When I relayed the information to my dev team they couldn’t really believe it either.
If the problem is the reconciliation of XML-Script blocks (i.e. there being several blocks which target the same controls) could the Atlas Framework not handle this in a number of different ways – one idea I had is to have an XmlScript container panel which is a server side control that allows you to manually write XML Script, at the PreRender stage of the ASP.NET lifecycle the contents of these containers are merged into a single master XML Script block.
Another possible solution could be to implement a Response Filter – a lovely feature in ASP.NET which allows you perform processing on the Response Stream just before it is sent to the client. I came up with a simple piece of code that parses the Response Stream, locates all instances of XML Script block and merges them into a single block. I took this idea and ran with it a bit:
using System;
using System.Data;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
public class XmlScriptFilter : Stream
{
private Stream sink;
private string mergedScriptBlock;
public XmlScriptFilter(Stream sink)
{
this.sink = sink;
}
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return true; }
}
public override bool CanWrite
{
get { return true; }
}
public override long Length
{
get { return this.sink.Length; }
}
public override long Position
{
get { return this.sink.Position; }
set { this.sink.Position = value; }
}
public override void Close()
{
this.sink.Close();
}
public override void Flush()
{
this.sink.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return this.sink.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
return this.sink.Seek(offset, origin);
}
public override void SetLength(long value)
{
this.sink.SetLength(value);
}
public override void Write(byte[] buffer, int offset, int count)
{
string localBuffer = UTF8Encoding.UTF8.GetString (buffer, offset, count);
List<string> scriptBlocks = new List<string>();
Regex scriptBlockRegExp = new Regex("<script\\stype=\"text/xml-script\">(?<content>.*?)</script>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
Regex formEndRegExp = new Regex("</form>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
MatchCollection matches = scriptBlockRegExp.Matches(localBuffer);
foreach (Match match in matches)
{
scriptBlocks.Add(match.Groups["content"].Value.Trim());
}
DataSet master = new DataSet();
foreach (string scriptBlock in scriptBlocks)
{
MemoryStream ms = new MemoryStream(UTF8Encoding.UTF8.GetBytes(scriptBlock));
DataSet duplicate = new DataSet();
duplicate.ReadXml(ms);
master.Merge(duplicate, true);
}
MemoryStream output = new MemoryStream();
master.DataSetName = "mergedScriptBlock";
master.WriteXml(output);
this.mergedScriptBlock = UTF8Encoding.UTF8.GetString(output.ToArray());
this.mergedScriptBlock = this.mergedScriptBlock.Replace("<mergedScriptBlock>", "<script type=\"text/xml-script\">\n<page xmlns:script=\"http://schemas.microsoft.com/xml-script/2005\">");
this.mergedScriptBlock = this.mergedScriptBlock.Replace("</mergedScriptBlock>", "</page>\n</script>");
localBuffer = scriptBlockRegExp.Replace(localBuffer, new MatchEvaluator (this.OnScriptBlockReplaceMatch));
localBuffer = formEndRegExp.Replace(localBuffer, new MatchEvaluator(this.OnFormEndReplaceMatch));
byte[] data = UTF8Encoding.UTF8.GetBytes(localBuffer);
this.sink.Write (data, 0, data.Length);
return;
}
private string OnScriptBlockReplaceMatch(Match m)
{
return string.Empty;
}
private string OnFormEndReplaceMatch(Match m)
{
return this.mergedScriptBlock + Environment.NewLine + "</form>";
}
}
To enable a Response Filter to process the Response Stream you need to host it inside an HttpModule:
namespace HvR.Prototype
{
using System;
using System.Web;
public class XmlScriptFilterHttpModule : IHttpModule
{
public void Init(HttpApplication app)
{
app.ReleaseRequestState += new EventHandler(InstallResponseFilter);
}
private void InstallResponseFilter(object sender, EventArgs e)
{
HttpResponse response = HttpContext.Current.Response;
if (response.ContentType == "text/html")
{
response.Filter = new XmlScriptFilter(response.Filter);
}
}
public void Dispose()
{
}
}
}
To configure a HttpModule to be executed you need to add the following to the <httpModules> section of the web applications web.config section:
<add name="XmlScriptFilterHttpModule" type="HvR.Prototype.XmlScriptFilterHttpModule"/>
This code can merge the two XML Script blocks generated by the ASPX above:
<script tpe="text/xml-script">
<page xmlns:script="http://schemas.microsoft.com/xml-script/2005">
<components>
<checkBox id="master"/>
<checkBox id="slave1">
<bindings>
<binding dataContext="master" dataPath="checked" property="checked" />
</bindings>
</checkBox>
<checkBox id="slave2">
<bindings>
<binding dataContext="master" dataPath="checked" property="checked" />
</bindings>
</checkBox>
<checkBox id="slave3">
<bindings>
<binding dataContext="master" dataPath="checked" property="checked" />
</bindings>
</checkBox>
</components>
</page>
</script>
and
<script type="text/xml-script">
<page xmlns:script = "http://schemas.microsoft.com/xml-script/2005" xmlns:atlascontrolextender = "atlascontrolextender" xmlns:atlascontroltoolkit = "atlascontroltoolkit">
<references>
<add src = "/Demos/WebResource.axd?d=-PvIfoXJkSxUQPFxwz28b- 3XJ2jLF4HeHUT2ynxU3kIRs3WEYYoWAflMmMJA5ypdI_E4yMoGjOs- Sg_9bvDQBpEu4VCiN7NMIlZ57dtRGow1&amp; t=632823506800000000" />
<add src = "/Demos/WebResource.axd?d=-EobaHj1wGJu84ic- rMVeaKp8EZKbknm9ABnL-5aEiLH6wy05MNMOFvGv8xl- E3gNKkfyQvA_rFC1CpBqITTZwEH1AyGUWoZ1k14XovVCus1&amp; t=632852634134593184" />
</references>
<components>
<pageRequestManager id = "_PageRequestManager" updatePanelIDs = "" asyncPostbackControlIDs = "" scriptManagerID = "ScriptManager1" form="form1" />
<checkBox id="master">
<behaviors>
<atlascontroltoolkit:toggleButton ImageHeight="15" UncheckedImageUrl = "ToggleButton_Unchecked.gif" CheckedImageUrl = "ToggleButton_Checked.gif" ImageWidth="15" />
</behaviors>
</checkBox>
<checkBox id="slave1">
<behaviors>
<atlascontroltoolkit:toggleButton ImageHeight = "15" UncheckedImageUrl = "ToggleButton_Unchecked.gif" CheckedImageUrl = "ToggleButton_Checked.gif" ImageWidth="15" />
</behaviors>
</checkBox>
<checkBox id="slave2">
<behaviors>
<atlascontroltoolkit:toggleButton ImageHeight = "15" UncheckedImageUrl = "ToggleButton_Unchecked.gif" CheckedImageUrl = "ToggleButton_Checked.gif" ImageWidth="15" />
</behaviors>
</checkBox>
</components>
</page>
</script>
into a single Script Block:
<script type="text/xml-script">
<page xmlns:script="http://schemas.microsoft.com/xml-script/2005" xmlns:atlascontrolextender = "atlascontrolextender" xmlns:atlascontroltoolkit = "atlascontroltoolkit">
<components>
<checkBox id="master">
<behaviors>
<atlascontroltoolkit:toggleButton ImageHeight="15" UncheckedImageUrl = "ToggleButton_Unchecked.gif" CheckedImageUrl = "ToggleButton_Checked.gif" ImageWidth = "15" xmlns:atlascontroltoolkit = "atlascontroltoolkit" />
</behaviors>
</checkBox>
<checkBox id="slave1">
<bindings>
<binding dataContext="master" dataPath="checked" property="checked" />
</bindings>
<behaviors>
<atlascontroltoolkit:toggleButton ImageHeight="15" UncheckedImageUrl = "ToggleButton_Unchecked.gif" CheckedImageUrl = "ToggleButton_Checked.gif" ImageWidth = "15" xmlns:atlascontroltoolkit = "atlascontroltoolkit" />
</behaviors>
</checkBox>
<checkBox id="slave2">
<bindings>
<binding dataContext="master" dataPath="checked" property="checked" />
</bindings>
<behaviors>
<atlascontroltoolkit:toggleButton ImageHeight="15" UncheckedImageUrl = "ToggleButton_Unchecked.gif" CheckedImageUrl = "ToggleButton_Checked.gif" ImageWidth = "15" xmlns:atlascontroltoolkit = "atlascontroltoolkit" />
</behaviors>
</checkBox>
<checkBox id="slave3">
<bindings>
<binding dataContext="master" dataPath="checked" property="checked" />
</bindings>
</checkBox>
<pageRequestManager id="_PageRequestManager" updatePanelIDs="" asyncPostbackControlIDs="" scriptManagerID="ScriptManager1" form="form1" />
</components>
<references>
<add src = "/Demos/WebResource.axd?d=-PvIfoXJkSxUQPFxwz28b- 3XJ2jLF4HeHUT2ynxU3kIRs3WEYYoWAflMmMJA5ypdI_E4yMoGjOs-Sg_9bvDQBpEu4VCiN7NMIlZ57dtRGow1&amp; t=632823506800000000" />
<add src = "/Demos/WebResource.axd?d=-EobaHj1wGJu84ic-rMVeaKp8EZKbknm9ABnL- 5aEiLH6wy05MNMOFvGv8xl- E3gNKkfyQvA_rFC1CpBqITTZwEH1AyGUWoZ1k14XovVCus1&amp; t=632852634134593184" />
</references>
</page>
</script>
There are still a few issues with this approach (namespaces seem to be a little confused) - but it's a start. More later.