Changing a Control’s Output Dynamically
A question that pops up occasionally in ASP.NET forums or sites like Stack Overflow is how to change a control’s output dynamically. Well, since the CSS Friendly Control Adapters came out, I knew how to do it statically, through static registration on a .browser file (it can also be done dynamically, for all controls of a certain type, by adding a fully qualified type name to HttpContext.Current.Request.Browsers.Adapters collection), but I decided to go and try to do it dynamically, without registration. More specifically, I wanted to achieve two things:
- Apply a XSL transformation to the generated output, provided it is XML-compliant;
- Change the output through code in an event handler-like fashion.
So, here’s what I came up with:
1: <%@ Register Assembly="WebApplication1" Namespace="WebApplication1" TagPrefix="web" %>
2: ...
3: <web:OutputAdapterControl runat="server" TargetControlID="table" OnOutput="OnOutput" XslPath="~/table.xsl" />
4:
5: <asp:Table runat="server" ID="table"/>
I have two control declarations, one is for a well known asp:Table, and the other is for my OutputAdapterControl. On its declaration, I have a property that references the asp:Table through its id, one property for an external XSL transformation file and the final one for registering an event handler.
The code for the OutputAdapterControl is:
1: namespace WebApplication1
2: {
3: [NonVisualControl]
4: public class OutputAdapterControl : Control
5: {
6: private static readonly FieldInfo occasionalFieldsField = typeof(Control).GetField("_occasionalFields", BindingFlags.NonPublic | BindingFlags.Instance);
7: private static readonly FieldInfo flagsField = typeof(Control).GetField("flags", BindingFlags.NonPublic | BindingFlags.Instance);
8:
9: public OutputAdapterControl()
10: {
11: this.Enabled = true;
12: }
13:
14: public String XslPath
15: {
16: get;
17: set;
18: }
19:
20: public String TargetControlID
21: {
22: get;
23: set;
24: }
25:
26: public Boolean Enabled
27: {
28: get;
29: set;
30: }
31:
32: public event EventHandler<OutputEventArgs> Output;
33:
34: private ControlAdapter getControlAdapter(Control control)
35: {
36: Object flags = flagsField.GetValue(control);
37: MethodInfo setMethod = flags.GetType().GetMethod("Set", BindingFlags.NonPublic | BindingFlags.Instance);
38: setMethod.Invoke(flags, new Object[] { 0x8000 });
39:
40: Object occasionalFields = occasionalFieldsField.GetValue(control);
41: FieldInfo rareFieldsField = occasionalFields.GetType().GetField("RareFields");
42: Object rareFields = rareFieldsField.GetValue(occasionalFields);
43:
44: if (rareFields == null)
45: {
46: rareFields = FormatterServices.GetUninitializedObject(rareFieldsField.FieldType);
47: rareFieldsField.SetValue(occasionalFields, rareFields);
48: }
49:
50: FieldInfo adapterField = rareFields.GetType().GetField("Adapter");
51: ControlAdapter adapter = adapterField.GetValue(rareFields) as ControlAdapter;
52:
53: return (adapter);
54: }
55:
56: private void setControlAdapter(Control control, ControlAdapter controlAdapter)
57: {
58: Object occasionalFields = occasionalFieldsField.GetValue(control);
59: FieldInfo rareFieldsField = occasionalFields.GetType().GetField("RareFields");
60: Object rareFields = rareFieldsField.GetValue(occasionalFields);
61: FieldInfo adapterField = rareFields.GetType().GetField("Adapter");
62: adapterField.SetValue(rareFields, controlAdapter);
63: }
64:
65: internal void RaiseOutputEvent(OutputEventArgs e)
66: {
67: if (this.Output != null)
68: {
69: this.Output(this, e);
70: }
71: }
72:
73: protected override void OnPreRender(EventArgs e)
74: {
75: if ((this.Enabled == true) && (String.IsNullOrWhiteSpace(this.TargetControlID) == false))
76: {
77: Control control = this.FindControl(this.TargetControlID);
78: ControlAdapter controlAdapter = this.getControlAdapter(control);
79: OutputAdapterControlAdapter newAdapter = new OutputAdapterControlAdapter(this, controlAdapter, control, this.XslPath);
80:
81: this.setControlAdapter(control, newAdapter);
82: }
83:
84: base.OnPreRender(e);
85: }
86: }
87: }
And the code for the control adapter is:
1: namespace WebApplication1
2: {
3: public class OutputAdapterControlAdapter : ControlAdapter
4: {
5: private static readonly FieldInfo controlField = typeof(ControlAdapter).GetField("_control", BindingFlags.NonPublic | BindingFlags.Instance);
6: private static readonly MethodInfo controlRenderMethod = typeof(Control).GetMethod("Render", BindingFlags.NonPublic | BindingFlags.Instance);
7: private static readonly MethodInfo controlAdapterRenderMethod = typeof(ControlAdapter).GetMethod("Render", BindingFlags.NonPublic | BindingFlags.Instance);
8:
9: public OutputAdapterControlAdapter(OutputAdapterControl outputControl, ControlAdapter original, Control control, String xslPath)
10: {
11: this.OutputControl = outputControl;
12: this.Original = original;
13: this.XslPath = xslPath;
14: controlField.SetValue(this, control);
15: }
16:
17: protected OutputAdapterControl OutputControl
18: {
19: get;
20: private set;
21: }
22:
23: protected String XslPath
24: {
25: get;
26: private set;
27: }
28:
29: protected ControlAdapter Original
30: {
31: get;
32: private set;
33: }
34:
35: protected override void Render(HtmlTextWriter writer)
36: {
37: StringBuilder builder = new StringBuilder();
38: HtmlTextWriter tempWriter = new HtmlTextWriter(new StringWriter(builder));
39:
40: if (this.Original != null)
41: {
42: controlAdapterRenderMethod.Invoke(this.Original, new Object[] { tempWriter });
43: }
44: else
45: {
46: controlRenderMethod.Invoke(this.Control, new Object[] { tempWriter });
47: }
48:
49: if (String.IsNullOrWhiteSpace(this.XslPath) == false)
50: {
51: String path = HttpContext.Current.Server.MapPath(this.XslPath);
52:
53: XmlDocument xml = new XmlDocument();
54: xml.LoadXml(builder.ToString());
55:
56: builder.Clear();
57:
58: XslCompiledTransform xsl = new XslCompiledTransform();
59: xsl.Load(path);
60: xsl.Transform(xml, null, tempWriter);
61: }
62:
63: OutputEventArgs e = new OutputEventArgs() { Html = builder.ToString() };
64:
65: this.OutputControl.RaiseOutputEvent(e);
66:
67: if (e.Html != builder.ToString())
68: {
69: builder.Clear();
70: builder.Append(e.Html);
71: }
72:
73: writer.Write(builder.ToString());
74: }
75: }
76: }
Finally there’s the event argument:
1: namespace WebApplication1
2: {
3: [Serializable]
4: public class OutputEventArgs : EventArgs
5: {
6: public String Html
7: {
8: get;
9: set;
10: }
11: }
12: }
And a sample event handler:
1: protected void OnOutput(object sender, OutputEventArgs e)
2: {
3: e.Html = e.Html.ToUpper();
4: }
Please note that this relies heavily on reflection, and, for that matter, will only work with ASP.NET 4.0. It can be made to work with ASP.NET 2/3.5, but you will have to change the code to get to the stored control adapter in the Control class. Thanks to João Angelo for reminding me of that!
If you look at it carefully, you will see that, if an XSL transformation file is specified, the control adapter will use it to transform the output of the target control. Either way, if an event handler is registered, it will raise the event, passing in the event argument the HTML generated by the target control, possibly modified by the XSL transformation. In the event handler, all you have to do is change the Html property of the event argument.
Let me know if this helps in some way, and have fun!