Making the case for an XamlViewEngine

What does a view engine need to meet today’s demands for an effective user interface?

Composition

Composition is probably the single most important factor for meeting today’s demands. One level of composition for a Web Forms developer is a MasterPage. A MasterPage allows all the pages of a website to have a consistent centralized layout. Whilst nested MasterPages are allowed in Web Forms, I believe they become a greater challenge to maintain.

In the XamlViewEngine, a region, would be the substitute to an ASP.NET masterpage.

Custom controls implementing ITemplate provide a second level of composition. By default, ASP.NET Web Forms require all controls implement their own Render(HtmlTextWriter output) function. Hence, to support this type of composition, the control developer must provide properties to supply ITemplate from the consumer’s view. He also has to include additional logic to render ITemplate when supplied, or use the default when not. In some cases, ITemplate comes with a delicate contract that requires certain types of controls to be integrated into the template. One example of ITemplate can be seen by reviewing a System.Web.UI.WebControls.Login control, and studying the LayoutTemplate. Additional samples of this type of composition can be found here. The book, ASP.NET 3.5 Unleashed, by Stephen Walther (2008), discusses custom controls in-depth.

This is an example of a custom panel control that implements ITemplate. The view contains an easy to read markup while the control’s responsiblity is to render the Yahoo YUI Panel.

View Markup

          <ui:Panel ID="YuiPanel1" runat="server" IsMaximumViewPortSize="true" IsShownByDefault="false">
	<ToggleButtons>
		<ui:ToggleButton ControlID="HyperLink1" OnEventName="click" />
	</ToggleButtons>
	<HeaderTemplate>
		Title Text
	</HeaderTemplate>
	<BodyTemplate>
			<asp:Repeater ID="Repeater1" runat="server">
				<ItemTemplate>
					<div style="margin-bottom:25px;">
						<asp:GridView ID="GridView1" AutoGenerateColumns="true" DataSource='<%# Container.DataItem %>' runat="server">
						</asp:GridView>
					</div>
				</ItemTemplate>
			</asp:Repeater>
	</BodyTemplate>
	<FooterTemplate>
		Footer Text
	</FooterTemplate>
</ui:Panel>

        

Custom Control Implementing ITemplate.

          using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Web;
using System.Web.UI;

using NClientScriptManager;

namespace NYUI.Controls
{
  [ParseChildren(true)]
  public class Panel : NClientScriptManager.CompositeControl
  {
    public Panel()
    {
      this.IsMaximumViewPortSize = false;
      this.IsShownByDefault = false;
      this.IsAligned = false;

      this.ShowButtons = new List<ShowButton>();
      this.HideButtons = new List<HideButton>();
      this.ToggleButtons = new List<ToggleButton>();

      this.PreRender += new EventHandler(Panel_PreRender);
    }


    [PersistenceMode(PersistenceMode.InnerProperty)]
    public List<ShowButton> ShowButtons
    {
      get;
      set;
    }

    [PersistenceMode(PersistenceMode.InnerProperty)]
    public List<HideButton> HideButtons
    {
      get;
      set;
    }

    [PersistenceMode(PersistenceMode.InnerProperty)]
    public List<ToggleButton> ToggleButtons
    {
      get;
      set;
    }




    public Boolean IsShownByDefault
    {
      get;
      set;
    }

    public Boolean IsMaximumViewPortSize
    {
      get;
      set;
    }

    public Boolean IsAligned
    {
      get;
      set;
    }

    public String AlignedToControlID
    {
      get;
      set;
    }


    void Panel_PreRender(object sender, EventArgs e)
    {
      this.Controls.Add(
        new ExternalCssReference()
        {
          Media = ExternalCssReference.MediaTypes.All,
          Path = "NYUIResourceHandler.ashx?path=yui.fonts.fonts-min.css"
        }
      );

      this.Controls.Add(
        new ExternalCssReference()
        {
          Media = ExternalCssReference.MediaTypes.All,
          Path = "NYUIResourceHandler.ashx?path=yui.container.assets.skins.sam.container.css"
        }
      );

      this.Controls.Add(
        new ExternalCssReference()
        {
          Media = ExternalCssReference.MediaTypes.All,
          Path = "NYUIResourceHandler.ashx?path=yui.resize.assets.skins.sam.resize.css"
        }
      );




      this.Controls.Add(
        new ExternalJsReference()
        {
          Path = "NYUIResourceHandler.ashx?path=yui.utilities.utilities.js"
        }
      );
      this.Controls.Add(
        new ExternalJsReference()
        {
          Path = "NYUIResourceHandler.ashx?path=yui.container.container-min.js"
        }
      );
      this.Controls.Add(
        new ExternalJsReference()
        {
          Path = "NYUIResourceHandler.ashx?path=yui.resize.resize-min.js"
        }
      );




      String javascript = @"
       <script type='text/javascript'>
         YAHOO.util.Event.onDOMReady(

        function() {


          // Create a panel Instance, from the '" + this.ClientID + @"' DIV standard module markup
          var panel = new YAHOO.widget.Panel('" + this.ClientID + @"', {
            draggable: true,
            constraintoviewport: true,
            " + (!this.IsMaximumViewPortSize ? "" : @" width: '" + this.Width.Value.ToString() + @"px',") + @"
            " + (!this.IsMaximumViewPortSize ? "" : @" height: '" + this.Height.Value.ToString() + @"px',") + @"
            " + (!this.IsAligned || String.IsNullOrEmpty(this.AlignedToControlID) ? "" : @"context: ['" + this.NamingContainer.FindControl(this.AlignedToControlID).ClientID + @"', 'tl', 'bl'],") + @"
            visible: false
          });
          panel.render();

          // Create Resize instance, binding it to the '" + this.ClientID + @"' DIV
          var resize = new YAHOO.util.Resize('" + this.ClientID + @"', {
            handles: ['br'],
            autoRatio: false,
            minWidth: 300,
            minHeight: 100,
            status: false
          });

          // Setup startResize handler, to constrain the resize width/height
          // if the constraintoviewport configuration property is enabled.
          resize.on('startResize', function(args) {

            if (this.cfg.getProperty('constraintoviewport')) {
              var D = YAHOO.util.Dom;

              var clientRegion = D.getClientRegion();
              var elRegion = D.getRegion(this.element);

              resize.set('maxWidth', clientRegion.right - elRegion.left - YAHOO.widget.Overlay.VIEWPORT_OFFSET);
              resize.set('maxHeight', clientRegion.bottom - elRegion.top - YAHOO.widget.Overlay.VIEWPORT_OFFSET);
            } else {
              resize.set('maxWidth', null);
              resize.set('maxHeight', null);
            }

          }, panel, true);

          // Setup resize handler to update the Panel's 'height' configuration property
          // whenever the size of the '" + this.ClientID + @"' DIV changes.

          // Setting the height configuration property will result in the
          // body of the Panel being resized to fill the new height (based on the
          // autofillheight property introduced in 2.6.0) and the iframe shim and
          // shadow being resized also if required (for IE6 and IE7 quirks mode).
          resize.on('resize', function(args) {
            var panelHeight = args.height;
            this.cfg.setProperty('height', panelHeight + 'px');
          }, panel, true);


          " + (this.IsMaximumViewPortSize?  @"
          panel.cfg.setProperty('height', YAHOO.util.Dom.getViewportHeight() - 25 + 'px');
          panel.cfg.setProperty('width', YAHOO.util.Dom.getViewportWidth() - 25 + 'px');
          ": "")+ @"


          " + (this.IsShownByDefault? "panel.show();": "") + @"

          " + this.AddClientEventHandlers() + @"


        }
      );
      </script>
      ";

      this.Controls.Add(
        new InHeader(){
          TextTemplate=new InHeaderTextTemplate()
          {
            Text = javascript
          }
        }
      );


      String css = @"
      <style type='text/css'>
        #" + this.ClientID + @" .bd {
          overflow:auto;
          background-color:#fff;
          padding:10px;
        }

        #" + this.ClientID + @" .ft {
          height:15px;
          padding:0;
        }

        #" + this.ClientID + @" .yui-resize-handle-br {
          right:0;
          bottom:0;
          height: 8px;
          width: 8px;
          position:absolute;
        }

        #" + this.ClientID + @"_c.hide-scrollbars .yui-resize .bd {
          overflow: hidden;
        }

        #" + this.ClientID + @"_c.show-scrollbars .yui-resize .bd {
          overflow: auto;
        }
        #" + this.ClientID + @"_c.show-scrollbars .underlay {
          overflow: visible;
        }
      </style>
      ";

      this.Controls.Add(
        new InHeader()
        {
          TextTemplate = new InHeaderTextTemplate()
          {
            Text = css
          }
        }
      );
    }

    [PersistenceMode(PersistenceMode.InnerProperty)]
    public ITemplate HeaderTemplate
    {
      get;
      set;
    }

    [PersistenceMode(PersistenceMode.InnerProperty)]
    public ITemplate BodyTemplate
    {
      get;
      set;
    }

    [PersistenceMode(PersistenceMode.InnerProperty)]
    public ITemplate FooterTemplate
    {
      get;
      set;
    }



    public String AddClientEventHandlers()
    {
      String html = String.Empty;

      //ShowButton
      foreach (ShowButton s in this.ShowButtons)
      {
        html += @"
        YAHOO.util.Event.addListener('" + this.NamingContainer.FindControl(s.ControlID).ClientID + @"', '" + s.OnEventName + @"', function(e, obj) {
          obj.show();
          YAHOO.util.Event.preventDefault(e);
        }, panel);
        ";
      }

      //HideButton
      foreach (HideButton h in this.HideButtons)
      {
        html += @"
        YAHOO.util.Event.addListener('" + this.NamingContainer.FindControl(h.ControlID).ClientID + @"', '" + h.OnEventName + @"', function(e, obj) {
          obj.hide();
          YAHOO.util.Event.preventDefault(e);
        }, panel);
        ";
      }

      //ToggleButton
      foreach (ToggleButton t in this.ToggleButtons)
      {
        html += @"
        YAHOO.util.Event.addListener('" + this.NamingContainer.FindControl(t.ControlID).ClientID + @"', '" + t.OnEventName + @"', function(e, obj) {

          YAHOO.util.Event.preventDefault(e);

          if(obj.cfg.getProperty('visible') == true){
            obj.hide();
          }
          else
          {
            obj.show();
          }

        }, panel);
        ";
      }

      return html;
    }


    protected override void CreateChildControls()
    {
      this.Controls.Add(
        new LiteralControl() {
          Text = @"
            <div class='yui-skin-sam'>
              <div id='"+this.ClientID+@"'>
                <div class='hd'>
          "
        }
      );

      if(HeaderTemplate != null)
        HeaderTemplate.InstantiateIn(this);

      this.Controls.Add( new LiteralControl() { Text = @"</div>"} );
      this.Controls.Add(
        new LiteralControl()
        {
          Text = @"
                <div class='bd'>
          "
        }
      );

      if (BodyTemplate != null)
        BodyTemplate.InstantiateIn(this);

      this.Controls.Add(new LiteralControl() { Text = @"</div>" });
      this.Controls.Add(
        new LiteralControl()
        {
          Text = @"
                  <div class='ft'>
                "
        }
      );

      if (FooterTemplate != null)
        FooterTemplate.InstantiateIn(this);


      this.Controls.Add(new LiteralControl() { Text = @"</div>" });


      this.Controls.Add(
        new LiteralControl()
        {
          Text = @"
            </div>
          </div>"
        }
      );
    }
  }
}

        

In the XamlViewEngine, the substitute to an ASP.NET ITemplate, is the framework’s ControlTemplate and DataTemplate. These distinct and separate templates form the basis for the next section Separation of Concerns.

Separation of Concerns

ControlTemplate

Display controls fall into ItemsControls (Accordian, Tab, TreeView, …) and ContentControl (Label, ModalPopup, …).

Input controls such as TextBox, Captcha, Calendar, and DatePicker can directly extend Control.

On Web 2.0/3.0/5.0, users can expect interactive websites, i.e. page flakes and mail.google.com. To accomplish these interactions, controls need to save control specific data; hence a service contract should be registered and separated from existing MVC routes handling business logic. Most controls also reference other assets, such as images, CSS, and JavaScript. Hence, the framework should supply a script manager to remove duplicate references of CSS and Javascript files. In Web Forms, the concept of INamingContainer exists, so that behavior can be attached to specific controls, thereby avoiding conflicts in HTML.

Ideally a developer should want simple views to show his intent.

          <ui:Window IsOpened="True">
	<ui:Window.Header>
		<h1>Hello World!</h1>
	</ui:Window.Header>
	<ui:Window.Content>
		<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
	</ui:Window.Content>
</ui:Window>

<ui:TabControl Background="#dde1ea">
	<ui:TabItem>
		<ui:TabItem.Header>
			<h1>Hello World!</h1>
		</ui:TabItem.Header>
		<ui:TabItem.Content>
			<p>Hello world.</p>
		</ui:TabItem.Content>
	</ui:TabItem>
	<ui:TabItem>
		<ui:TabItem.Header>
			<h1>Hello Universe!</h1>
		</ui:TabItem.Header>
		<ui:TabItem.Content>
			<p>Hello universe.</p>
		</ui:TabItem.Content>
	</ui:TabItem>
</ui:TabControl>

        

The code for a Window and/or TabControl would be hidden in a registered ‘Style’. The ‘Style’ would specify whether to render the Window and TabControl using AJAX Control Toolkit, YUI, Ext Js, or jQuery UI. The bound properties (IsOpened, Background) would be available to the ‘Style’ through TemplateBinding.

To summarize, a Control would need to support a Style, a concept of INamingContainer (to avoid conflicts if the same control is used on the same page), and a service contract (for data persistence for highly interactive UI dashboards).

DataTemplate

MVVM, Model-View-ViewModel reached MVC after the following ViewModel MVC tip was published. Since all views are given a ‘Model’ this would, in XAML speak, be the DataContext of the view.

          <ui:TabControl Background="#dde1ea">
	<ui:TabItem>
		<ui:TabItem.Header>
			<strong>Edit My Profile</strong>
		</ui:TabItem.Header>
		<ui:TabItem.Content>
			<ui:ContentControl Template="{StaticResource areaEditDataTemplate}" />
		</ui:TabItem.Content>
	</ui:TabItem>
	<ui:TabItem>
		<ui:TabItem.Header>
			<strong>View My Profile</strong>
		</ui:TabItem.Header>
		<ui:TabItem.Content>
			<ui:ContentControl Template="{StaticResource areaViewDataTemplate}" />
		</ui:TabItem.Content>
	</ui:TabItem>
</ui:TabControl>

        

The actual view template for the viewmodel could be defined inside an MVC area as follows.

          <ResourceDictionary
	xmlns:mvc="http://..../presentation/mvc"
	>
	<DataTemplate x:Name="ReadOnlyView">
		<%!-- similar to a render partial  --%>
		${Binding FirstName, Converter={StaticResource _camelCase}}  <br />
		${Binding LastName, Converter={StaticResource _camelCase}} <br />
	</DataTemplate>
	<DataTemplate x:Name="EditView">
		<%!-- similar to a render partial  --%>
		<mvc:TextBox For="{Binding FirstName}" Value="{Binding FirstName}" /> <br />
		<mvc:TextBox For="{Binding LastName}" Value="{Binding LastName}" /> <br />
	</DataTemplate>
</ResourceDictionary>

        

View Engines need to be testable

The problem is that LoadControl which compiles a given view requires the integration of IIS, Load Web Forms, and UserControls from Embedded Resources. The parser/control builder, though, is not provided externally for testing facilitation.

Conclusion

This is by no means a comprehensive case study of challenges that could be solved by the XamlViewEngine. Hopefully, though, it will get the ball moving, and help solve the view engine problem.

I’ve only done the base XAML parser to date, which is 5% of this project. XamlViewEngine