NuGet package | Owin.Framework.Pages.Html |
GitHub source | OwinFramework.Pages.Html |
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 creates and configures a component for each template.
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 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 with very little code.
Components have the following features:
There are some situations in which you might want to define a component with no C# implementation because all of the required funcionality can be achieved through adding attributes to the component. For example:
[IsComponent("defaultStyles")] [PartOf("application")] [DeployedAs("content")] [DeployCss("p", "font-size:11pt;")] [DeployCss("h1", "font-size:16pt;")] [DeployCss("h2", "font-size:14pt;")] [DeployCss("h3", "font-size:12pt;")] internal class DefaultStylesComponent { }
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 attributes that you can attach to a component are:
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 only once when rendering all other parts of the page. For example if I put the same component on my page 3 times, the body page area will be written 3 times (in each of the locations where I placed the component) but anything that this component renders in the page head, initialization area etc will only be written once.
If the component is inside of a region that repeats its content, this is the same as adding the component multiple times. Each time the region repeats the body area will be written again, but the page head will only be written once.
To be more specific, 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:
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:
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.
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.
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:
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; } }