Background Color:
 
Background Pattern:
Reset
Search
Home Recent Changes Show All Pages

Custom Complex Binding Component

Not Rated Yet

In this article we will walk through creating a custom component that can take a string value to be parsed into more scryber components and added to a page, so that it we can data bound complex structures rather than just simple values. We will create the custom component class in our own library, an associated schema for the class, and then add it to a test document to check the result.

 

Article Contents

 

About the scryber parser

 

As previously mentioned the in An Introduction to the XML Parser, scryber is completely dynamic. The parser has no knowledge of what a component is, or how a scryber document should be constructed. It simply uses explicit attributes on classes to match against the xml data.

This means that there is no dependancy on the parser and the released set of components. We can add our own components, and expect the parser to simply infer the types and properties.

 

A new project and class

 

For our new component we are going to create a new .Net class library, and add a new component class to it. Our 'spec' details that we want a component that can take an xml fragment string either explicitly during the generation, or data bound and then have this parsed into any set of components which will then be rendered on the page.

 

So let's start by creating a new solution and associated class library (called Scryber.Extensions in the example) in Visual Studio.

We are also going to need some test project to make sure that what we code will work, either web or forms, but in this example we can use an empty web project (Scryber.Extensions.WebTest).

And we are going to need a reference to the scryber library itself, so the easiest way will be to add a scryber pdfx document to our web project, as long as the vsix package is installed. This will give us our schemas, references and a test file for our own component.

Finally let's reference back to the scryber dlls in the package for our Scryber.Extensions project. So we can use the libraries when building our component. We should now have something similar to below.

 


 

Class, properties and attributes

 

Now we need to add our custom component class, that will contain one or more inner components, we can call it a structure for content - So PDFStructure is a good class name.

The class needs to be able to contain other components, but we want it to simply display the inner content and not contribute to the appearance or layout. Looking at the hierarchy in Component Basics there is a PDFContainerComponent which has the content capability, there is also the PDFVisualComponent, but this starts contributing to the style of a hierarchy, so is too complex. Let's keep with the container

And because we want the flow through we should also implement the IPDFInvisibleComponent place holder interface. This means the layout engine will not create any block or line to hold the inner contents, they will be at the same level in the layout as the placeholders siblings.

Having our class, inheritance and interface(s) we need to declare that we are actually involved in the parsing process by decorating the class as a parsable component with the Scryber.PDFParsableComponentAttrbute.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Scryber.Components;
using Scryber.Data;

namespace Scryber.Extensions
{
     [PDFParsableComponent("Structure")]
     public class PDFStructure : PDFContainerComponent, IPDFInvisibleContainer
     {


     }
}

 

We now need to add a few properties to our class to hold the required information. The first thing we need is the property to hold the string that we should parse when we need to generate the components. Again the parser requires an opt-in attribute, rather than opt-out. So we need to decorate our property with the Scryber.PDFAttributeAttribute so we can have the string data as an attribute on the Structure component.

Because the parser needs to know about assemblies based on namespaces, we will need to tell the parser the namespaces and prefixes we use in the xml snippet, so we will have to declare these. We do the same with the PDFXMLDataSource class, so we can re-use this class - Scryber.Data.PDFXmlNamespaceCollection. This also means there is no required dependancy between the snippet and the containing document.

We also need to add a constructor as the PDFContainerComponent does not have an empty constructor, it requires a PDFObjectType which is a set of 4 characters that programatically identifies the sort of object we are dealing with to the library

By convention all uppercase object types are native PDF Types - Number, real, names, string and all core library object types are lowercase. Mixed case are open for use and abuse without causing any impact

Adding a bit of logic to ensure we have a collection instance and we do not inadvertently change the parsing contents after we have already been built, we can get the code below. (Final source is available at the end of the article).

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Scryber.Components;
using Scryber.Data;

namespace Scryber.Extensions
{
	///<summary>
	/// A parsable component class that can contain other components, but does not contribute to the actual layout itself.
	/// The actual contents of the structure can be set from the string ParsableContents property
	///</summary>
	[PDFParsableComponent("Structure")]
    public class PDFStructure : PDFContainerComponent, IPDFInvisibleContainer
    {

        bool _parsed = false;
        string _data;

        /// <summary>
        /// Gets or sets the string contents of the placeholder that will be parsed into scryber components.
        /// </summary>
        [PDFAttribute("contents")]
        public string ParsableContents
        {
            get { return _data; }
            set
            {
                if (_parsed)
                    throw new InvalidOperationException("Already parsed the structure contents. Cannot update the content");

                _data = value;
                _parsed = false;
            }
        }

        private PDFXmlNamespaceCollection _nss;

        /// <summary>
        /// Gets the list of namespaces that will be used in the parsable contents so they can be identified and used.
        /// </summary>
        [PDFArray(typeof(PDFXmlNamespaceDeclaration))]
        [PDFElement("Namespaces")]
        public PDFXmlNamespaceCollection Namespaces
        {
            get
            {
                if (null == _nss)
                    _nss = new PDFXmlNamespaceCollection();
                return _nss;
            }

        }

        /// <summary>
        /// Creates a new instance of the PDFStructure class to hold components
        /// </summary>
        public PDFStructure()
            : base((PDFObjectType)"Strc")
        {
        }

    }
}

 

 

Implementing the string parsing

 

Whenever the scryber library needs to build components from a string in data templates, it uses an instance of the Scryber.Data.PDFParsableTemplateGenerator to create the components, and this fits our needs here too. It's constructor expects a component (this instance), some string xml content which we have, and an XmlNamespaceManager (which we can build from our PDFXmlNamespaceCollection). It is then simply a call to the Instantiate method with an index and it will generate any number of components that are in the string. We can then add these returned components to our own instances InnerContent collection (and mark ourself as parsed)

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Scryber.Components;
using Scryber.Data;

namespace Scryber.Extensions
{
     [PDFParsableComponent("Structure")]
     public class PDFStructure : PDFContainerComponent, IPDFInvisibleContainer
     {
		.
		.
		.
		.
		.
		.
		
	     protected virtual void EnsureContentsParsed()
	     {
	         if (!this._parsed && !string.IsNullOrEmpty(this._data))
	         {
	             System.Xml.XmlNamespaceManager mgr = GetNamespaces();

	             Scryber.Data.PDFParsableTemplateGenerator gen = new Data.PDFParsableTemplateGenerator(this, this._data, mgr);

	             IEnumerable<IPDFComponent> all = gen.Instantiate(0);
	             foreach (IPDFComponent child in all)
	             {
	                 this.InnerContent.Add((PDFComponent)child);
	             }
	             _parsed = true;
	         }
	     }

	     private System.Xml.XmlNamespaceManager GetNamespaces()
	     {
	         System.Xml.NameTable nt = new System.Xml.NameTable();
	         System.Xml.XmlNamespaceManager mgr = new System.Xml.XmlNamespaceManager(nt);

	         if (null != this._nss)
	         {
	             foreach (PDFXmlNamespaceDeclaration dec in _nss)
	             {
	                 mgr.AddNamespace(dec.Prefix, dec.NamespaceURI);
	             }
	         }
	         return mgr;
	     }
	

     }
}

 

So our only question now is when in the document lifecycle we should call this method (see Document Processing). We definitely need to call it before the layout starts, as the components would not then be part of the output, but if we do it before the data binding, then any value that is set on the ParsableContents property during data bind would be ignored. Because we are checking the parsed and actual data values in our EnsureContentsParsedMethod, we are able to call this any number of times. So lets' do it twice - once after the component has been loaded, and once after the component is databound. Just to be sure we capture it

 

namespace Scryber.Extensions
{
     [PDFParsableComponent("Structure")]
     public class PDFStructure : PDFContainerComponent, IPDFInvisibleContainer
     {
		.
		.
		.
		.
		.
		.

	
		protected override void OnLoaded(EventArgs e)
		{
		    this.EnsureContentsParsed();

		    base.OnLoaded(e);
		}

		protected override void OnDataBound(PDFDataBindEventArgs e)
		{
		    base.OnDataBound(e);

		    this.EnsureContentsParsed();
		}

		protected virtual void EnsureContentsParsed()
		{

		.
		.
		.
		.
		.
		.    

     }
}

 

Go ahead and build the library to make sure it compiles as a minimum. As long as it does, we now have a complete PDFStructure class that will take it's parsable contents and build the inner components based on the string data. But if we add it to our test document now, it should work, but we will get Xml schema errors and no intellisense. So let's create the schema file too.

 

Creating a new schema

 

The scryber schemas have been designed, just like the framework, to be extensible. There are some core abstract types and substitution groups that can be used to include new elements and they will automatically be picked up by intellisense. Therefore we can add a new xsd schema template to our Scryber.Extensions project and add namespaces and imports for the existing schemas (you may need to alter the import location yourself for the actual files to be found)

 

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="Scryber_Extensions_Structure"
    targetNamespace="Scryber.Extensions, Scryber.Extensions"
    xmlns="Scryber.Extensions, Scryber.Extensions"
    elementFormDefault="qualified"
    xmlns:pdf="Scryber.Components, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:style="Scryber.Styles, Scryber.Styles, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
    xmlns:data="Scryber.Data, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe">

  <!-- 
    Includes 
  -->

  <xs:import namespace="Scryber.Components, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
            schemaLocation="../Scryber.Extensions.WebTest/Schemas/Scryber/Scryber.Components.v08.xsd" />
  <xs:import namespace="Scryber.Styles, Scryber.Styles, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
             schemaLocation="../Scryber.Extensions.WebTest/Schemas/Scryber/Scryber.Styles.v08.xsd" />
  <xs:import namespace="Scryber.Data, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
              schemaLocation="../Scryber.Extensions.WebTest/Schemas/Scryber/Scryber.Data.v08.xsd" />


</xs:schema>

 

Because our component is in the Scryber.Extensions dll and has a namespace of Scryber.Extensions - the target namespace of our schema needs to include this information so the parser can find the component.

We hare not signing our assembly, and are not interested in the version, so we can just use the short [Namespace], [Assembly file name] notation, rather than the full signed assembly name. But using the full name does uniquely identify any assembly if desired

If we look at the existing Scryber.Components schema, we know our Structure element will be usable wherever we can use a label, or a span, and these elements have a substitution group of ScryberContentBase, and the type declarations extend the ScryberContentBaseType. So we can use these to support the page content.

We also know that the PDFXmlDataSource has a Namespaces child element. If we look at the Scryber.Data schema we can see this is of type Binding-XmlNamespacesType so, although we could write our own, it is easier and more sustainable if we simply reference this type.

Finally we need to add our 'contents' attribute, and also the standard element attributes (id, visible, name).

 

<?xml version="1.0" encoding="utf-8"?>
<xs:schema id="Scryber_Extensions_Structure"
    targetNamespace="Scryber.Extensions, Scryber.Extensions"
    xmlns="Scryber.Extensions, Scryber.Extensions"
    
	.
	.
	.
	
	
  <!--
    PlaceHolder element
  -->

  <xs:complexType name="StructureType" >
    <xs:complexContent>
      <xs:extension base="pdf:ScryberContentBaseType" >
        <xs:sequence minOccurs="0" maxOccurs="1" >
          <xs:element name="Namespaces" type="data:Binding-XMLNamespaces" form="unqualified" />
        </xs:sequence>
        <xs:attribute name="contents" type="pdf:BindableStringType" />
        <xs:attributeGroup ref="pdf:ElementAttributes" />
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>


  <xs:element name="Structure" type="StructureType" substitutionGroup="pdf:ScryberContentBase" ></xs:element>


</xs:schema>

 

We should now have a complete library and XmlSchema that can be used to generate complex content. Let's go ahead and validate.

 

Testing the Component

 

Turning our attention to the WebTest project, we still need to reference our Scryber.Extensions project so the dll is included, so add that reference, and then we can start modifying the pdfx document we added.

 

We need to add our namespace with a prefix to the top level of the document: xmlns:extn='Scryber.Extensions, Scryber.Extensions and then we can add our Structure element to the document. We should get full intellisense whilst adding the Structure component

 

<?xml version="1.0" encoding="utf-8" ?>
<pdf:Document xmlns:pdf="Scryber.Components, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
              xmlns:style="Scryber.Styles, Scryber.Styles, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
              xmlns:data="Scryber.Data, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
              xmlns:extn="Scryber.Extensions, Scryber.Extensions"
              id="TestDocument" compression="Compress" auto-bind="true">
  <Styles>

    <style:Style applied-id="titlepage" >
      <style:Font size="14pt" />
      <style:Text leading="20pt"/>
      <style:Margins all="20pt" />
    </style:Style>

  </Styles>
  <Pages>

    <pdf:Page id="titlepage" >
      <Content>
        Hello World.
        <extn:Structure id="MyStructre" contents="This is some &lt;pdf:B&gt;complex&lt;/pdf:B&gt; content."  >
          <Namespaces>
            <data:Xmlns prefix="pdf" namespace="Scryber.Components, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"/>
          </Namespaces>
        </extn:Structure>
        This is after the structure
      </Content>
    </pdf:Page>

  </Pages>
</pdf:Document>

 

We then need to add an aspx page in our WebTest Project. Add a button and invoke the document parsing on click.

 

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="Scryber.Extensions.WebTest.Index" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Button runat="server" ID="GeneratePDFButton" Text="Generate PDF" OnClick="GeneratePDFButton_Click" />
    </div>
    </form>
</body>
</html>

 

protected void GeneratePDFButton_Click(object sender, EventArgs e)
{
    using (Scryber.Components.PDFDocument doc = Scryber.Components.PDFDocument.ParseDocument("TestDocument.pdfx"))
    {
        doc.ProcessDocument(this.Response);
    }
}

 

All we need to do now is test the component and make sure it works. Launch the site and click the button.

 


 

We can see there is some content coming from the the Structure and the inner bold content is parsed and output. In this example we could just have easily added the content inline, but it does validate that our component is actually working.

 

Complex binding

 

Databinding is implemented at the parser level, rather by the component itself, so out of the box can have binding statements applied to it, and it's attributes. To extend our example and make it more useful we can apply the content dynamically - in this case using the item notation (see Item and Query String Binding). We could just as easily include our Structure component in a ForEach data loop and use the {xml:[xpath]} notation.

 

    <pdf:Page id="titlepage" >
      <Content>
        Hello World.
        <extn:Structure id="MyStructre" contents="{item:ComplexDescription}"  >
          <Namespaces>
            <data:Xmlns prefix="pdf" namespace="Scryber.Components, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"/>
          </Namespaces>
        </extn:Structure>
        This is after the structure
      </Content>
    </pdf:Page>

 

    protected void GeneratePDFButton_Click(object sender, EventArgs e)
    {
        using (Scryber.Components.PDFDocument doc = Scryber.Components.PDFDocument.ParseDocument("TestDocument.pdfx"))
        {
            doc.Items["ComplexDescription"] = "This is some <pdf:B>item bound complex </pdf:B> content. ";
            doc.ProcessDocument(this.Response);
        }
    }

If we build and generate this new content we should see the new data coming from the items collection that we set in our code behind. And we are still parsing the content properly

 


 

Styled components in the Structure

 

Let's really go to town and put some good complex content in our structure and see how it handles it. We will also need to add the style namespace declaration to our Structure component on the document.

 

    protected void GeneratePDFButton_Click(object sender, EventArgs e)
    {
        protected void GeneratePDFButton_Click(object sender, EventArgs e)
        {
            using (Scryber.Components.PDFDocument doc = Scryber.Components.PDFDocument.ParseDocument("TestDocument.pdfx"))
            {
                doc.Items["ComplexDescription"] = @"<pdf:Div style:padding='10pt' style:bg-color='#DDDDDD' >
            This is some <pdf:B>item bound complex </pdf:B> content.
        </pdf:Div>
        <pdf:Table style:class='custom-table' >
          <pdf:Row>
            <pdf:Cell  style:border-color='gray' style:padding='10' >This is the first cell</pdf:Cell>
            <pdf:Cell  style:border-color='gray' style:padding='10' >This is the second cell</pdf:Cell>
          </pdf:Row>
          <pdf:Row>
            <pdf:Cell  style:border-color='gray' style:padding='10' >This is the thrid cell</pdf:Cell>
            <pdf:Cell  style:border-color='gray' style:padding='10' >This is the fourth cell</pdf:Cell>
          </pdf:Row>
        </pdf:Table>";
                doc.ProcessDocument(this.Response);
            }
        }
    }

 

<pdf:Document xmlns:pdf="Scryber.Components, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
              xmlns:style="Scryber.Styles, Scryber.Styles, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
              xmlns:data="Scryber.Data, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"
              xmlns:extn="Scryber.Extensions, Scryber.Extensions"
              id="TestDocument" compression="Compress" auto-bind="true">
  <Styles>

    <style:Style applied-id="titlepage" >
      <style:Font size="14pt" />
      <style:Text leading="20pt"/>
      <style:Margins all="20pt" />
    </style:Style>

    <style:Style applied-class="custom-table" >
      <style:Margins all="10pt"/>
    </style:Style>

  </Styles>
  <Pages>

    <pdf:Page id="titlepage" >
      <Content>
        Hello World.
        <extn:Structure id="MyStructre" contents="{item:ComplexDescription}"  >
          <Namespaces>
            <data:Xmlns prefix="pdf" namespace="Scryber.Components, Scryber.Components, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"/>
            <data:Xmlns prefix="style" namespace="Scryber.Styles, Scryber.Styles, Version=0.8.0.0, Culture=neutral, PublicKeyToken=872cbeb81db952fe"/>
          </Namespaces>
        </extn:Structure>
        This is after the structure
      </Content>
    </pdf:Page>

  </Pages>
</pdf:Document>

 


 

Structure Enhancements

 

It's all working well, perhaps the only things we could improve in our PDFStructure class are the default inclusion of the standard namespaces, rather than having to explicitly declare them each time, with a couple of properties that alter the prefix value (and because it's scryber, these are bindable too). And also allow programmatic access to the content collection in case we need to add items at runtime. These are added in the downloadable files associated with this article, and simplify our component without impacting functionality.

	<extn:Structure id="MyStructre" contents="{item:ComplexDescription}" 
	                        styles-prefix="style" />

 

Chances are we will include this component in the code library in a future release (as of 0.8.8.5) but before then, and as a good example of extending scryber, the article still stands.

 

Download the solution.

Download the solution filesDownload the entire solution, project and source files as a zip

Download the filesDownload just the class and schema files for inclusion in your own project.

See Also.

 



  Rating
Rate This Page: Poor Great   |  Rate Content |
Average rating:  No Ratings Yet   
Number of Ratings : 0
  Comments
Add Comment
No Comments Yet