Area

Pages Framework

Pages Framework Home page

Component Elements

Component elements are the leaves of the tree structure that defines an HTML page. The components are responsible for writing the HTML to the response that is sent back to the browser. The HTML to write is defined in C# code.

Note that you do not have to create C# classes for every piece of content, that would be extremely time consuming and hard to manage. There are higher level facilities that create components for you, for example the template loader and template parser mechanism and the CMS packages.

You will typically only write component classes to gain access to low level control over the HTML that is produced, for example to write custom headers into the <head></head> section of the page.

You can also write components to simplify your chosen scheme for editing and presenting content. For example this website that documents the Owin Framework has a component that maps the page URL onto a template path allowing me to define one page that renders hundreds of different templates creating all of the pages on the website.

Components have the following features:

  • You can set an array of lambda functions into the HtmlWriters property and they will be called during the rendering of the page body. This is how things like template parsers configure components to render the template.
  • You can set an array of lambda functions into the CssRules property and they will be called during the rendering of the CSS. The CSS can be rendered into the page head, or into a separate CSS file depending on the asset deployment mechanism selected.
  • You can set an array of lambda functions into the JavascriptFunctions property and they will be called during the rendering of the JavaScript. The JavaScript can be rendered into the page head, or into a separate JavaScript file depending on the asset deployment mechanism selected.
  • You can override the GetPageAreas method to indicate which areas of the page your component will write to. To improve page rendering efficiency the rendering engine will prune areas of the tree that contain no components that render to a specific portion of the page when that part of the page is being rendered.
  • You can override the GetCommentName method to provide a custom comment. These comments are written to the HTML when comments are turned on for debugging purposes.
  • You can override the WritePageArea method to output custom HTML in any area of the page where this component exists. Note that this is only guaranteed to be called for page areas returned by the GetPageAreas method.

Writing Custom HTML

To write custom HTML to anywhere on the page override the WritePageArea and GetPageAreas methods. For example:

    [IsComponent("person")]
    [NeedsData(typeof(Person))]
    internal class PersonComponent : Component
    {
        public PersonComponent(IComponentDependenciesFactory dependencies)
            : base(dependencies)
        {
        }

        public override IEnumerable<PageArea> GetPageAreas()
        {
            return new[] { PageArea.Body };
        }

        public override IWriteResult WritePageArea(IRenderContext context, PageArea pageArea)
        {
            if (pageArea == PageArea.Body)
            {
                var person = context.Data.Get<Person>();
                context.Html.WriteElementLine("p", person.Name);
            }
            return WriteResult.Continue();
        }
    }

The WritePageArea method is called once for each component instance on the page when rendering the body of the page, but when there are multiple components of the same type on a page, or the component is inside a repeating region the WritePageArea method is called only once when writing other parts of the page. For example if your component writes to the <head></head> section of the page and the component is included multiple times on a page, then the WritePageArea method will only be called once for each page with the pageArea parameter set to PageArea.Head.

The WritePageArea method must return an instance of the IWriteResult interface. You can implement this interface yourself, or you can use static methods of the WriteResult class. The purpose of the IWriteResult is to let the rendering engine know if or when you are finished writing your HTML. The static methods of WriteResult work like this:

  • WriteResult.Continue() means I have finished writing all of my Html but other components should be run to contribute output as well. This is the most frequently used use case.
  • WriteResult.ResponseComplete() means I have finished writing all of my Html and the response is now complete so do not render output from any other controls. This is mostly usefull when wtiring to the page title and you do not want to concatenate the results of all component that write to the page title.
  • WriteResult.ContinueAsync(action) means that my Html takes a long time to produce, so kick it off in a background thread and continue running other components. If other components also return WriteResult.ContinueAsync(action) then these operations will continue in parallel on different threads. This option requires some special programmming to ensure that the asyncrhronously produced Html ends up in the correct sequence in the response. See section on this below.
  • WriteResult.WaitFor(task) means that I started a background task to write my Html. Please wait for this task to complete before sending the response back to the browser. In the meantime the other components on the page will continue processing and outputting their Html. See section below on asyncrhronously writing to the Html output stream.

Consuming Data in your Component

If your component needs data from a data provider it is better NOT to inject the data provider directly or look in the data catalog for it. Instead you should decorate your class with the [NeedsData()] attribute. If you do it this way there are several advantages:

  • If you place multiple components on the page that need the same data the data provider will only be called once.
  • If you place the component inside a repeater that repeats the data it needs then it will work as expected.
  • If the dependant data is available with multiple scopes then the page designer can choose which scope to apply and your component will receive the correct version of the data.

Here is an example of a component that uses data. This example will render Html for an address

    [IsComponent("address")]
    [NeedsData(typeof(Address))]
    internal class AddressComponent : Component
    {
        public AddressComponent(IComponentDependenciesFactory dependencies)
            : base(dependencies)
        {
        }

        public override IWriteResult WritePageArea(IRenderContext context, PageArea pageArea)
        {
            if (pageArea == PageArea.Body)
            {
                var address = context.Data.Get<Address>();
                context.Html.WriteElementLine("p", address.Street);
                context.Html.WriteElementLine("p", address.City);
                context.Html.WriteElementLine("p", address.ZipCode);
            }
            return WriteResult.Continue();
        }
    }

The [NeedsData()] attribute causes the requested type of data to be added to the IRenderContext where you can retrieve it by calling the Get<T>() method of its Data property.

Asyncronous HTML Output

The Pages Framework is designed to be extremely flexible and scaleable and therefore prvides a mecahnism to allow components to contribute asynchronously to the Html output. Here is an example:

        public override IWriteResult WriteBodyArea(IRenderContext context)
        {
            var html = context.Html;

            // Save this location in the output buffer
            var begining = html.CreateInsertionPoint();

            // Write a paragraph of text
            html.WriteElementLine("p", "This is paragraph 2");

            // Write a paragraph of text in a background thread
            var task = Task.Factory.StartNew(() =>
                {
                    // Simulate a call to a service or database here
                    Thread.Sleep(10);

                    begining.WriteElementLine("p", "This is paragraph 1");
                });

            // Write a third paragraph of text
            html.WriteElementLine("p", "This is paragraph 3");

            return WriteResult.WaitFor(task);
        }

In this example the resulting web page will contain the three paragraphs in numeric order even though paragraph 1 was written after the other two.

Components as Services

You can decorate your component's class with the [IsService] attribute to allow it to handle AJAX calls and Postbacks from the browser. There are a few supported use cases as follows:

  • Make your component class inherit from the Service class then implement the IComponent inferface.
  • Make your component class inherit from the Component class but also add a default public constructor that can be used to construct the class as a service.
  • Make your component class inherit from the Component class but and pass the optional factory method when registering with the fluent builder. The builder will use the factory to construct a second instance of your component to act as the service.
This technique is especially useful for modularizing your AJAX calls because you can write a component the will write JavaScript functions to call the service, and also implement the service endpoints within the same class keeping these things close together. In this case adding the component to a website adds both the JavaScrpipt to call the service and the service endpoints.
Postbacks are a bit more tricky in that they have to re-render the page after handling the postback. Exactly how you do that depends a lot in the overall architecture of your solution.

Component Attributes

You can do some things with components without writing any code, this example is adapted from the menu package that is shipped with the Pages Framework:

        // Desktop menu behavior
        [DeployCss("ul.{ns}_dt_menu", "list-style-type: none; overflow: hidden; white-space: nowrap;", 1)]
        [DeployCss("li.{ns}_dt_option", "display: inline-block;", 2)]
        [DeployCss("li.{ns}_dt_option a, a.{ns}_dt_option", "display: inline-block; text-decoration: none;", 3)]
        [DeployCss("div.{ns}_dt_dropdown", "display: none; position: absolute; overflow: hidden; z-index: 1;", 4)]
        [DeployCss("div.{ns}_dt_dropdown a", "text-decoration: none; display: block; text-align: left", 5)]
        [DeployCss("li.{ns}_dt_option:hover div.{ns}_dt_dropdown", "display: block;", 6)]

        // Hamburger button behavior
        [DeployCss("input[type=checkbox].{ns}_mb_hamburger_button", "display: none;", 20)]
        [DeployCss("label.{ns}_mb_hamburger_button", "transition: all 0.3s; cursor: pointer; ", 21)]
        [DeployCss("div.{ns}_mb_hamburger_icon", "transition: all 0.3s; position: relative; float: left; width: 100%;", 22)]
        [DeployCss("input[type=checkbox].{ns}_mb_hamburger_button:checked + label > .{ns}_mb_hamburger_icon_1", "transition: all 0.3s; transform: rotate(135deg);", 23)]
        [DeployCss("input[type=checkbox].{ns}_mb_hamburger_button:checked + label > .{ns}_mb_hamburger_icon_2", "transition: all 0.3s; opacity: 0;", 23)]
        [DeployCss("input[type=checkbox].{ns}_mb_hamburger_button:checked + label > .{ns}_mb_hamburger_icon_3", "transition: all 0.3s; transform: rotate(-135deg);", 23)]

        // Slideout menu behavior
        [DeployCss("ul.{ns}_mb_slideout", "position: absolute;", 24)]
        [DeployCss("input[type=checkbox].{ns}_mb_hamburger_button:checked ~ ul.{ns}_mb_slideout", "transform: translateX(0);", 25)]
		[IsComponent("menu_styles")]
        public class MenuStyles
        { }

By decorating your component with the [IsComponent("component_name")] attribute you do not have to manually register each component with the Fluent Builder, instead you can ask the Fluent Builder to register everything in your assembly and it will use reflection to find all of the components.

The other attributes you can add are:

  • [DeployCss] css rules to include on any page that includes this component
  • [DeployFunction] a JavaScript function to include on any page that has this component on it
  • [DeployCss] css rules to include on any page that includes this component
  • [DeployedAs] specifies how JavaScript and CSS should be deployed for this component
  • [NeedsComponent] identifies a dependent component
  • [NeedsData] identifies data that is required for this component
  • [PartOf] defines the package (namespace) for CSS and JavaScript in this component
  • [RenderHtml] makes this component render localized HTML

Components in packages

You can create a reusable plug-in package. These packages can contain functionallity that can be added to a website with a single line of code. This technique is most often used to build open-source packages that are designed to be added to many websites and fulfill some common need, but you can also manage your own application using packages if you choose.

When you create a package, the package's Build method is called at startup. This is where the package creates all of the pages, layouts, components etc that comprise the package contents. This Build method is passed an instance of IFluentBuilder that can be used to build all of the package contents. The fluent builder has a namespace context that avoids namespace clashes with other packages and/or application code.

This is an example of using the fluent builder to build a component.

    public class CmsEditorPackage: IPackage
    {
        public CmsEditorPackage()
        {
            Name = "cms_editor";
            NamespaceName = "cmseditor";
        }

        IPackage IPackage.Build(IFluentBuilder fluentBuilder)
        {
			// ... some code omitted here for brevity
			
            var assetsComponent = fluentBuilder.BuildUpComponent(null)
                .Name("assets")
                .DeployIn(module)
                .DeployFunction(null, "initVue", null, script.ToString(), true)
                .RenderInitialization("cms-editor-init", "")
                .DeployLess(less.ToString())
				.Build();

            return this;
        }
	}

Note that the Owin Framework NuGet package installs the xml documentation file alongside the dlls so you can get intellisense for the methods available in the fluent syntax.

Note that you can combine all of these techniques together, for example you can write a class that inherits from Component but also overrides methods of the base class, and uses the fluent builder syntax. This is illustrated in the example below:

    public class LibrariesPackage : Framework.Runtime.Package
    {
        public LibrariesPackage(IPackageDependenciesFactory dependencies)
            : base(dependencies)
        {
            Name = "libraries";
            NamespaceName = "libraries";
        }

		[DeployedAs(AssetDeployment.InPage)]
        private class LibraryComponent : Component
        {
            public string[] Urls { get; set; }

            public LibraryComponent(IComponentDependenciesFactory dependencies, params string[] urls)
                : base(dependencies)
            {
                PageAreas = new[] { PageArea.Head };
                Urls = urls;
            }

            public override IWriteResult WritePageArea(IRenderContext context, PageArea pageArea)
            {
                if (pageArea == PageArea.Head)
                {
                    foreach (var url in Urls)
                        context.Html.WriteElementLine("script", string.Empty, "src", url);
                }

                return WriteResult.Continue();
            }
        }

        public override IPackage Build(IFluentBuilder builder)
        {
            builder.BuildUpComponent(
                new LibraryComponent(
                    Dependencies.ComponentDependenciesFactory, 
                    "https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"))
                .Name("jQuery1")
                .Build();

            return this;
        }
    }