Wednesday, 9 December 2009

Building an ASP.NET MVC E-Commerce app with Flux – Part 3

Now that we know the MVC application is able to get to data in Flux using the API (did we expect anything less), we can look at making this do something useful. In this article we will;

  • Introduce a link on the main site called “Help centre” which will take it’s content from a channel we will define (see previous article)
  • Implement a content type to define general content pages for the help centre
  • Implement a controller and view that will render these general content pages
  • Have the general content page list a table of contents where there is content beneath them in the hierarchy or render a link back to the parent if there is content above them.

With this, we’ll be able to re-create the help content sections that you would normally find on most e-commerce sites.

Before I kick off with this one though, I should point out a couple of things. In no way shape or form is this lot production ready code! In an effort to be concise, and to the point, you’ll find quite stupid things in here like:

image

Which in a production site is a no-no. We should treat the content we’re rendering as unsafe and un-trusted and not just render it out willy-nilly like this. Also, you’ll notice no polishing like WYSIWYG editing in this sample – see my previous post on how to achieve this using fckEditor. I leave the removal of stupidity and the polishing as an exercise for the reader :)

Ok, so, here’s the final product of this article;

image

and

image

1. The “Help Center” link

The easiest part here – we created the help center link in the master page as follows;

                <ul id="menu">              
                    <li><%= Html.ActionLink("Home", "Index", "Home")%></li>
                    <li><%= Html.ActionLink("Help Center", "HelpCenter", "Page")%></li>
                </ul>

This creates a link to the controller called PageController which will invoke the method HelpCenter. We then need to wire up the controller for this, but before we do any of that we may as well go ahead and setup flux to support the page content we need.

2. The new flux content type

In flux we define a new content type for a content type called “Page” by creating a new folder containing a new XML type config and the administration form, as below. We also modify the channel content type definition to tell it to allow “Page” types to be added as children.

image

The code for the page content.type.config is as follows;

<NodeType ID="Page" Name="Simple Page Content" LimitVersions="10">

    <AddForm>~/settings/types/Page/form.aspx</AddForm>
    <EditForm>~/settings/types/Page/form.aspx</EditForm>

    <Relationships AddRoot="false">
        <Add>Page</Add>
    </Relationships>

</NodeType>

And the form.aspx content:

<%@ Page Language="C#" Inherits="Deepcode.Flux.Core.UI.CMS.CMSContentForm" ValidateRequest="false"%>
<%@ Register TagPrefix="flux" Namespace="Deepcode.Flux.Core.UI.Controls" Assembly="Deepcode.Flux.Core"%>

<script runat="server">
    // Type ID being managed by this form
    protected override string FormTypeCode { get { return "Page"; } }

    // Do form setup
    protected override void SetupForm(int ContentID, int ParentID)
    {
        if (!Page.IsPostBack)
        {
            fPath.NodeIDParent = ParentID;
            fPath.NodeIDPathed = ContentID;
        }
    }

    // Save fields to content object
    protected override void SaveContent(ref Deepcode.Flux.Core.Systems.CMS.ContentObject save)
    {
        save.NodeName = this.fPath.Text;
        save.NodeTitle = this.fPageTitle.Text;
        save.Fields["body"] = this.fContent.Text;
    }

    // Load fields from content object
    protected override void LoadContent(Deepcode.Flux.Core.Systems.CMS.ContentObject load)
    {
        this.fPath.Text = load.NodeName;
        this.fPageTitle.Text = load.NodeTitle;
        this.fContent.Text = load.Fields["body"];
    }
</script>

<html>
<head id="Head1" runat="server">
    <link href="../../../admin/Asset/Style/GeneralStyle.css" rel="Stylesheet" type="text/css" />
</head>
<body class="nopadshaded">
<form id="form1" runat="server">
<flux:HostTable ID="HostTable1" runat="server">

<%-- Summary --%>
<flux:ValidationSummarySection ID="ValidationSummarySection1" runat="server" HeaderText="Please correct the following errors"/>

<%-- Form area --%>
    <flux:Section ID="Section1" runat="server" Title="Add/Edit Simple Page Content">
    <flux:ShadePadBox ID="ShadePadBox1" runat="server">

        <table cellspacing="0" cellpadding="3" border="0">
        <tr><td>Path:</td>
            <td><flux:NodePath runat="server" ID="fPath" Width="200px" MaxLength="100"/></td>
            </tr>

        <tr><td>Page Title:</td>
            <td><asp:TextBox runat="server" ID="fPageTitle" Width="300px" MaxLength="500"/></td>
            </tr>
        </table>

    </flux:ShadePadBox>
    </flux:Section>
    
    <flux:Section runat="server" Title="Page Content">
        <asp:TextBox runat="server" ID="fContent" Width="100%" Height="200px" TextMode="MultiLine"/>
    </flux:Section>

<%-- Buttons --%>
<flux:Section ID="Section3" runat="server">
<flux:PadBox ID="PadBox1" CssClass="Pad5Button" runat="server">
    <asp:Button runat="server" ID="btnSave" Text="Save" OnClick="btnSave_Click" CssClass="button"/>&nbsp;&nbsp;&nbsp;
    <asp:Button runat="server" ID="btnCancel" Text="Cancel" OnClick="btnCancel_Click" CausesValidation="False"/>
</flux:PadBox>
</flux:Section>

<%-- Validators --%>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ControlToValidate="fPath"
    ErrorMessage="You must specify the path for this page" Display="None"/>

<asp:RequiredFieldValidator ID="RequiredFieldValidator2" Runat="server" ControlToValidate="fPageTitle" 
    ErrorMessage="You must specify the title of this page" Display="None"/>


</flux:HostTable>

</form>
</body>
</html>

That then gives us this very simple management form in flux that we can use to define content.

image

We need to go ahead and create some content for our help and support centre. the way I chose to structure this is to expect a channel path of /content/articles and a page node within that with a name of “HelpHome”. This would be the main help and support centre content page, and the child pages within that would cover the individual topics (see the images at the beginning of the post) – go ahead and create some content now.

image

3. and 4. - Rendering the general content pages

Being MVC, we need to consider the model (the data we’re going to render), the controller (the, um, controller of the app) and the view (the aspx code to render the model).

The controller is implemented as follows;

using System.Web.Mvc;
using MVCStore.Models;

namespace MVCStore.Controllers
{
    [HandleError]
    public class PageController : Controller
    {
        readonly ContentModel _modelProvider = new ContentModel();

        [AcceptVerbs(HttpVerbs.Get)]
        public ActionResult HelpCenter()
        {
            return View("PageContent", _modelProvider.GetHelpCentre());
        }

        [AcceptVerbs(HttpVerbs.Get)]
        public ActionResult PageContent(int pageId)
        {
            return View("PageContent", _modelProvider.GetPageForNodeId(pageId));
        }
    }
}

Note the model provider – this is some utility code we’ll build shortly to query the Flux content database. Our two controller methods are HelpCenter - which will get the help centres home page and render it and PageContent – which will use the same mechanics to render any page given the id of the content element.

The content model, which is created in /Models in this case is a mechanism to get the CMS data and return it in a way that the view is expecting. The code for ContentModel is shown below;

using System;
using Deepcode.Flux.Core.Systems.CMS;
using MVCStore.Views.Page;

namespace MVCStore.Models
{
    /// <summary>
    /// Provider to get content out of Flux as required
    /// </summary>
    public class ContentModel
    {
        public PageModel GetHelpCentre()
        {
            ContentQuery query = new ContentQuery();

            // Get the "Page" content item under /content/articles/ with the name "HelpHome"
            query.BaseMatch.AND(new StaticFieldMatch(ContentStaticField.Path, ContentQueryOperator.EQ, "/content/articles/"));
            query.BaseMatch.AND(new StaticFieldMatch(ContentStaticField.ContentType, ContentQueryOperator.EQ, "Page"));
            query.BaseMatch.AND(new StaticFieldMatch(ContentStaticField.Name, ContentQueryOperator.EQ, "HelpHome"));

            // Ensure we have a matching row
            ContentObject [] results = query.GetMatching();
            if (results.Length < 1)
                throw new InvalidOperationException("/content/articles/HelpHome page content does not exist");

            return CreatePageModelForNode(results[0]);
        }

        public PageModel GetPageForNodeId(int nodeId)
        {
            ContentObject node = ContentQuery.GetByID(nodeId);

            // Ensure we have a matching row
            if (node == null)
                throw new InvalidOperationException("content does not exist");

            return CreatePageModelForNode(node);
        }

        private PageModel CreatePageModelForNode(ContentObject node)
        {
            int parentNodeId = -1;
            
            // Only specify a parent IF it's also an item of content
            if (node.ParentLive.FK_STypeID == "Page")
                parentNodeId = node.FK_ParentID;

            PageModel result = new PageModel
            {
                ContentNodeId = node.PK_ID,
                PageContent = node.Fields["body"].Replace("\n", "<br/>"),
                PageTitle = node.NodeTitle,
                ParentNodeId = parentNodeId
            };

            foreach (ContentObject child in node.ChildrenLive)
                result.AddChild(child.PK_ID, child.NodeTitle);

            return result;
        }
    }
}

The GetHelpCentre method uses the content query engine exposed from flux to query for content within the channel path /content/articles, where the type of the entity is Page with a name of HelpHome – this corresponds to the help home page we prescribed earlier. This then assembles from the CMS data through to a PageModel, which is a view specific model that I’ll get to in a second.

The other method, GetPageForNodeId will also return a PageModel but for the content item with the given id number.

The view specific PageModel is;

using System.Collections.Generic;

namespace MVCStore.Views.Page
{
    /// <summary>
    /// View model for rendering page content with table of contents if there are children
    /// and rendering a link back to the parent if there is a parent....
    /// </summary>
    public class PageModel
    {
        public PageModel()
        {
            Children = new List<KeyValuePair<int, string>>();
        }

        /// <summary>
        /// The content id of this node
        /// </summary>
        public int ContentNodeId { get; set; }
        
        /// <summary>
        /// The content id of the parent node
        /// </summary>
        public int ParentNodeId { get; set; }
        
        /// <summary>
        /// The title of the page
        /// </summary>
        public string PageTitle { get; set; }
        
        /// <summary>
        /// The content for the page
        /// </summary>
        public string PageContent { get; set; }

        /// <summary>
        /// The children for this page that we want to list in a TOC
        /// the pairs are node id and page title.
        /// </summary>
        public IList<KeyValuePair<int, string>> Children { get; set; }

        /// <summary>
        /// Adds a child.
        /// </summary>
        /// <param name="nodeId">The node id.</param>
        /// <param name="title">The title.</param>
        public void AddChild(int nodeId, string title)
        {
            Children.Add(new KeyValuePair<int, string>(nodeId, title));
        }
    }
}

The final piece of the puzzle is the view itself, which is extremely simplistic;

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<PageModel>" %>
<%@ Import Namespace="MVCStore.Views.Page"%>

<asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
    
    <%if( Model.ParentNodeId != -1 ){ %>
        <%=Html.ActionLink("Up to parent", "PageContent", "Page", new { pageId = Model.ParentNodeId }, null)%>
    <%}%>
    
    <h2><%=Model.PageTitle%></h2>
    <%=Model.PageContent%>
    
    <%if( Model.Children.Count > 0 ){ %>
    
        <h3>Table of Contents</h3>
        <ul>
            <%foreach( KeyValuePair<int, string> toc in Model.Children ){ %>
            <li><%=Html.ActionLink(toc.Value, "PageContent", "Page", new { pageId = toc.Key }, null)%></li>
            <%}%>
        </ul>
    <%}%>

</asp:Content>

The code is again checked into codeplex here (http://fluxmvcstore.codeplex.com/) to bring you to this point, along with an SQL data script to get the content database up to speed.

I hope this is proving useful. I hope to have the next in the series available over the weekend.

Dont make me WAIT!!! DB Pro - you suck…..

RANT time…. I’m staring at visual studio 2008 at the moment and I’ve been staring at it for the last 25 minutes. I can’t interact with it as it’s busy doing some very complex operation – renaming a folder in a db pro project….. absolute twoddle!!!!!!

Tuesday, 8 December 2009

Building an ASP.NET MVC E-Commerce app with Flux.net as the management tool – Part 2

Continuing the series I started yesterday, today we’ll look at getting some basic content structures defined in Flux along with executing some basic queries for that data from the MVC application.

As ever, follow along or get the source from codeplex here. If you’re using the source, don’t forget to re-create your database so the data in your app matches that created through this article.

Channel content type

As we’re going to be pulling data out of Flux from MVC without using URL re-writing or anything like that, we need a mechanism to address content in flux. We could use the data taxonomy tools to tag data in flux and query based on this, but this lacks form and structure, so I decided to define a channel content type that allows us to expose paths to data that we can easily query against.

For example, when we come to want to find the root categories for our site we’ll perhaps query against /categories and this will have entries for each category, which in turn will have products. We can then extend this later with channels like /content/news to define news articles we might want to list or /content/about to define a list of articles we want to show on the site.

The definition for the channel content type couldn’t be simpler – in the management project, we create a folder at /settings/types called Channel and create the following Content.type.config file;

<NodeType ID="Channel" Name="Content Channel" LimitVersions="1">

    <AddForm>~/settings/types/Channel/form.aspx</AddForm>
    <EditForm>~/settings/types/Channel/form.aspx</EditForm>

    <Relationships AddRoot="true">
        <Add>Channel</Add>
    </Relationships>

</NodeType>

We then add the form.aspx file to manage this content type;

<%@ Page Language="C#" Inherits="Deepcode.Flux.Core.UI.CMS.CMSContentForm" ValidateRequest="false"%>
<%@ Register TagPrefix="flux" Namespace="Deepcode.Flux.Core.UI.Controls" Assembly="Deepcode.Flux.Core"%>

<script runat="server">
    // Type ID being managed by this form
    protected override string FormTypeCode { get { return "Channel"; } }

    // Do form setup
    protected override void SetupForm(int ContentID, int ParentID)
    {
        if (!Page.IsPostBack)
        {
            fPath.NodeIDParent = ParentID;
            fPath.NodeIDPathed = ContentID;
        }
    }

    // Save fields to content object
    protected override void SaveContent(ref Deepcode.Flux.Core.Systems.CMS.ContentObject save)
    {
        save.NodeName = this.fPath.Text;
        save.NodeTitle = this.fPageTitle.Text;
    }

    // Load fields from content object
    protected override void LoadContent(Deepcode.Flux.Core.Systems.CMS.ContentObject load)
    {
        this.fPath.Text = load.NodeName;
        this.fPageTitle.Text = load.NodeTitle;
    }
</script>

<html>
<head id="Head1" runat="server">
    <link href="../../../admin/Asset/Style/GeneralStyle.css" rel="Stylesheet" type="text/css" />
</head>
<body class="nopadshaded">
<form id="form1" runat="server">
<flux:HostTable ID="HostTable1" runat="server">

<%-- Summary --%>
<flux:ValidationSummarySection ID="ValidationSummarySection1" runat="server" HeaderText="Please correct the following errors"/>

<%-- Form area --%>
    <flux:Section ID="Section1" runat="server" Title="Add/Edit Content Channel">
    <flux:ShadePadBox ID="ShadePadBox1" runat="server">

        <table cellspacing="0" cellpadding="3" border="0">
        <tr><td>Path:</td>
            <td><flux:NodePath runat="server" ID="fPath" Width="200px" MaxLength="100"/></td>
            </tr>

        <tr><td>Page Title:</td>
            <td><asp:TextBox runat="server" ID="fPageTitle" Width="300px" MaxLength="500"/></td>
            </tr>
        </table>

    </flux:ShadePadBox>
    </flux:Section>

<%-- Buttons --%>
<flux:Section ID="Section3" runat="server">
<flux:PadBox ID="PadBox1" CssClass="Pad5Button" runat="server">
    <asp:Button runat="server" ID="btnSave" Text="Save" OnClick="btnSave_Click" CssClass="button"/>&nbsp;&nbsp;&nbsp;
    <asp:Button runat="server" ID="btnCancel" Text="Cancel" OnClick="btnCancel_Click" CausesValidation="False"/>
</flux:PadBox>
</flux:Section>

<%-- Validators --%>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ControlToValidate="fPath"
    ErrorMessage="You must specify the path for this page" Display="None"/>

<asp:RequiredFieldValidator ID="RequiredFieldValidator2" Runat="server" ControlToValidate="fPageTitle" 
    ErrorMessage="You must specify the title of this page" Display="None"/>


</flux:HostTable>

</form>
</body>
</html>

And our basic channel type is defined. We should now be able to use the manager to setup some basic channels – login to http://localhost:55000/ as admin@fluxcms.co.uk with a password of “password” and create some channels as below and then check them all in.

 image

The next thing we need to do is get the MVC application to be able to see this data – to do this we add a reference in the MVC app to Deepcode.Flux.Core.dll (which you’ll find in management/bin). This gives us access to the Flux API, but before we can call it we also need to setup the flux.config in the MVC application. For this I created the /settings directory in the MVC application and copied flux.config from the other project.

Now, we can call the API. In the controller, I added the following (not very elegant) code;

public ActionResult Index()
{
    ContentObject [] list = ContentQuery.GetForParentID(-1);
    ViewData["list"] = list;
    return View();
}

Here, we’re doing a very basic query for all content at the root level, which should give us the categories and content nodes. For now, rather than creating a real model, I’ve just pumped this into view data ready for rendering, and in the view code I added;

    <%foreach(ContentObject obj in (ContentObject []) ViewData["list"]){%>
        <p><%=obj.NodeName%></p>
    <%}%>

Which, … drum roll please …, gives us:

image

WHOOT! Our MVC app is getting it’s data from flux. So we’ve proven the point, we just now need to make it do something useful – but I’ll save that for the next post.

Monday, 7 December 2009

Building an ASP.NET MVC E-Commerce app with FLUX.net as the management tool

A friend of mine was recently contemplating using FLUX to build out an e-commerce application for a store. Whilst this is all well and good, and certainly very plausible, I started to think about how e-commerce sites are generally structured – they aren’t so much about open ended content where you add pages and articles together to form a site, they are far more rigid – more like structured catalogues with prescribed functionality.

As I’m also a fan of the MVC framework (I use it lots in my day job) I’ve been looking for an excuse to build something that uses MVC to render content fed from Flux.NET and I figure this is a good enough opportunity to explore this through a series of blog posts.

The site concept

We’ll manage products and categories in one website, which is standard ASP.NET and using the Flux content management system, then render these to our MVC store front application that has a prescribed structure by querying the content database for data in the appropriate places. Whereas we might normally use flux to dictate the form and structure, in this instance we’re going to use it purely as a management tool for data in our application.

Getting started

I began by downloading the flux starter package and creating a new empty solution. I added an ASP.NET website to the solution and then extracted the starter kit to it, trimming it right down to almost nothing (see image below) and reconfiguring /settings/flux.config to use a new database I created – FluxMVCTest. I ran the flux database script against this db to create my tables etc and then cleared down all the content through the admin tool.

image

With this running and responding on the development web server, I was reasonably happy to proceed and get the MVC site up in the solution too. I added this using the usual new project wizard and then stripped it back to it’s bare essentials also, which at this point gives me two sites in my solution as below;

Follow along source on codeplex

As usual, I’ve started a new project in codeplex for this, so hop along to http://fluxmvcstore.codeplex.com to get the source code so far.

Sunday, 15 November 2009

Overcoming cross domain issues between frames

In an application that I am building at the moment, I had need to “mash-up” content from different sources into a SharePoint site. This SharePoint site would;

  1. Use an iframe to pull in a list of content from another server
  2. Launch a modal dialog (AJAX style) overlaid on top of the SharePoint portal with an iframed form to action against the data
  3. Display validation errors from that form as another modal dialog (owned by the parent SharePoint page so as not to be size limited to the owning dialog).

A picture says a thousand words;

 image

In this case, we’d want the SharePoint host page to offer two methods

  • launchTool(url) – which will launch the given URL in an iframe within a modal jquery overlay (using boxy in my instance).
  • displayValidation(message) – which will be called by the form iframe to display the validation messages if the form can’t be submitted

And this is where we have a problem. If everything was on the same server, it would all be quite happy with this scenario and all the components would play together nicely but in this instance the content is spread across different servers. In this case, we’re not allowed to build this level of interaction using JavaScript as the content doesn’t fulfil the “same origin policy” – which means content from different domains can’t interrogate each others DOM nor invoke methods etc against each other.

image

Working around this isn’t just as simple as putting the sites into a different zone in the browser etc, we have to do something a little more innovative. If you are in total control of your infrastructure you can set all of the content servers to have a fully qualified domain name – eg: sp.mysite.com and forms.mysite.com and then use the document.domain property in JavaScript to set the domain to the higher level domain ie: mysite.com (document.domain = “mysite.com”) – but this does cause problems in my scenario where the SSRS report viewer web part in sharepoint doesn’t work (it does some funky stuff under the covers with iframes which fails miserably when we set document.domain in the host pages).

So, what can we do?

Well, the content from formserver can’t call anything within the sharepoint page and vice versa, but the formserver content could somehow create an iframe of it’s own with a special url on the sharepoint site that then calls the javascript we’re looking for in the sharepoint page. Clear as mud? No? here’s another diagram.

image

So, the list iframe doesn’t go back up the window stack to ask it’s parent to do something (which it has no rights to), it instead goes down – creating it’s own iframe and navigating it to a known page on the host and passes information to the page using a # bookmark. This known page then uses javascript on it’s page load event to determine what to do with the data passed in. If this indicates it should launch a modal dialog for a given URL, it passes the request to the overall parent, which, because it’s on the same domain, can be accessed.

It’s important to note the # part of the URL – by doing this, the cross domain receiver page can be fully cached in the browser, meaning there should be no lag between making the request down the stack to the parent actually processing that request.

I’ve wrapped the above into an API and built a sample that does the above, but also supports bi-directional message passing – so our host page has a list from another domain, it asks the host to display a form from yet another domain and this form requires confirmation, so it asks the host to display a message with options – the result of this message is then passed back down to the form.

Get the source code here: http://xdsframes.codeplex.com

(This isn’t a finished API in any way, just a starting off point for solving your own cross domain issues.)

Saturday, 3 October 2009

Flux.NET quick start package released

As promised, I've just wrapped up the content of my previous post, with some extras to produce a FLUX.NET quick start package. You can get it now on codeplex : http://flux.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=26826#DownloadId=86138

To use it:

  1. Unzip the distribution into a folder on disk
  2. Create an application in IIS that maps to this folder (To work out of the box, the application should be hosted on localhost. If it isn’t, you will need to update the domain mappings in the website mapping tool in flux.)
  3. Create an SQL database called FluxStarterKit (You can actually call it whatever you like but you must change the connection strings in /settings/flux.config)
  4. Execute /documentation/flux.startup.sql against the SQL database

That’s it.

To view the website:

  1. Navigate to the IIS website you created above

To open the admin tool:

  1. Navigate to the IIS website you created above + /admin
  2. Login as admin@fluxcms.co.uk with a password of password

10 Step guide to getting a basic FLUX.NET CMS site up.

Today I decided to start a new site for a friend of mine, and being the owner of the FLUX.NET open source CMS, I obviously decided to use that. I figured I'd use this opportunity though to document a step by step guide to how to get flux.net setup from download to a single basic page being up and running, so here goes.

Step 1 - get the distro!

Go to http://flux.codeplex.com and get the latest release package (v2.0 release here)

Step 2 - Copy the files into your site

1. From the distribution, copy the /Src/flux_client folder into the folder that will be the root of your site

image

2. Next go into /Src/Deepcode.Flux.Admin/and copy the entire contents into a /admin directory in your site

image

3. Move the bin directory from admin, 1 level down, outside of the admin directory.

4. Finally, copy one of the web.config files from the distribution into your site. These are in the root of the distribution and are named FormsWeb.Config and WindowsWeb.Config. In this instance, I want to use forms authentication so I'll take FormsWeb.Config, copy it into my site and rename it to web.config.

5. At this point, you should have a site directory structure as follows;

image

Step 3 - configuring IIS and SQL for local development

1. Open IIS manager, right click on the default website and select to create a new application (I'm using IIS7 in this exercise):

image

2. Give the virtual directory an alias to use, set it's application pool to classic .NET app pool and point the physical path to the directory you've created in step 2.4 above.

image

3. Open SQL Server and create a database for your content. (You can get the installer tool to create it for you, but I prefer to create it by hand)...

Step 4 - install the application

1. Navigate to http://localhost/youralias/admin/setup/install to start the installation process and click next to open the setup database form:

image

2. Enter the parameters necessary to connect to the database you created earlier. For my local development environment, I setup my server as ".", fluxtest as the database name, ticked the database has already been created checkbox,  and specified windows authentication as the SQL authentication mechanism. Click next to go to authentication setup.

 image

3. As we're aiming for forms authentication here, I selected "Forms authentication"  and setup the details of my super user to correspond to myself. Click next to go to general setup;

image

4. Enter a name for the installation (which will appear in the administration tools), along with a default mail server and an address which flux can send email as. Click next to review your installation settings.

image

5. Click finish to begin installation. At this point the system will install the database scripts and generate configuration files. As such, the app pool users of your web application must have write access to the location of your site and the SQL user/windows user also must have permission to create tables and stored procedures etc in your database. Once complete, you should see a screen like that below and a flux.config file will appear in a new folder called settings in your application.

image

6. If you "click here to continue" flux is now completely setup and you should be able to access the admin tool and it's modules etc as below;

image

However, the application itself doesn't know anything about content at this point.... we need to configure it to make it do something next.

Step 5 - opening your site in visual studio.

In order to do anything useful we will need to open visual studio 2008 and open the web site. Open it directly from the local IIS installation. If it offers to update it to .NET 3.5, say yes so that we can utilise LINQ and other cool features in our rendering templates. With this done, you should now have an environment ready to roll;

image

Step 6 - the worlds most basic content type

Back in flux, If you open "content management" and select to add a new item of content, you will notice that it doesn't give you any options of what to add. That is because flux is completely agnostic of what data makes up content. It understands content concepts and how content should relate etc, but not necessarily what fields make up what types of content, for this we must give it some more information.

For the purposes of this demonstration we're going to build a CMS that can host multiple web sites, but only has basic HTML pages available to it. This keeps the scope simple so that I can actually complete this blog post in one sitting!!!!

1. In VS.NET Create a new folder under /settings called "types" and a folder within this called "Site".

2. Add a new XML file to this folder and name it "Site.type.config". Then paste the content below into it;

<NodeType ID="Site" Name="Website" LimitVersions="1">

    <AddForm>~/settings/types/Site/form.aspx</AddForm>
    <EditForm>~/settings/types/Site/form.aspx</EditForm>

    <Relationships AddRoot="true">
        <Add>Page</Add>
    </Relationships>

    <StaticData>
        <Template>~/settings/types/Site/render.aspx</Template>
    </StaticData>

</NodeType>

This defines a content type that we can add to our system. LimitVersions="1" tells flux only to keep a maximum of 1 version history of anything that is of this type. The Add/Edit form specifies the ASPX templates that will be used to add or edit content items of this type, which we'll build shortly. The relationships section determines how this content type relates to other items. In this case, we tell flux that sites can be added to the root node and within them, a content type called page can be added. Finally the static data template item specifies the rendering template when content of this type is rendered in html.

3. Now we need to create the ASPX file that will let us add and edit content in flux. Create a new web form called form.aspx in the /settings/types/Site directory and set it's content as follows;

<%@ Page Language="C#" Inherits="Deepcode.Flux.Core.UI.CMS.CMSContentForm" ValidateRequest="false"%>
<%@ Register TagPrefix="flux" Namespace="Deepcode.Flux.Core.UI.Controls" Assembly="Deepcode.Flux.Core"%>

<script runat="server">
    // Type ID being managed by this form
    protected override string FormTypeCode { get { return "Site"; } }

    // Do form setup
    protected override void SetupForm(int ContentID, int ParentID)
    {
        if (!Page.IsPostBack)
        {
            fChannelPath.NodeIDParent = ParentID;
            fChannelPath.NodeIDPathed = ContentID;
        }
    }

    // Save fields to content object
    protected override void SaveContent(ref Deepcode.Flux.Core.Systems.CMS.ContentObject save)
    {
        save.NodeName = this.fChannelPath.Text;
        save.NodeTitle = this.fChannelName.Text;
    }

    // Load fields from content object
    protected override void LoadContent(Deepcode.Flux.Core.Systems.CMS.ContentObject load)
    {
        this.fChannelName.Text = load.NodeTitle;
        this.fChannelPath.Text = load.NodeName;
    }
</script>

<html>
<head id="Head1" runat="server">
    <link href="../../../admin/Asset/Style/GeneralStyle.css" rel="Stylesheet" type="text/css" />
</head>
<body class="nopadshaded">
<form id="form1" runat="server">
<flux:HostTable ID="HostTable1" runat="server">

<%-- Summary --%>
<flux:ValidationSummarySection ID="ValidationSummarySection1" runat="server" HeaderText="Please correct the following errors"/>

<%-- Form area --%>
    <flux:Section ID="Section1" runat="server" Title="Add/Edit Website channel">
    <flux:ShadePadBox ID="ShadePadBox1" runat="server">

        <table cellspacing="0" cellpadding="3" border="0">
        <tr><td>Channel Name:</td>
            <td><asp:TextBox runat="server" ID="fChannelName" Width="300px" MaxLength="500"/></td>
            </tr>
        <tr><td>Channel Path:</td>
            <td><flux:NodePath runat="server" ID="fChannelPath" Width="200px" MaxLength="100"/></td>
            </tr>
        </table>

    </flux:ShadePadBox>
    </flux:Section>

<%-- Buttons --%>
<flux:Section ID="Section2" runat="server">
<flux:PadBox ID="PadBox1" CssClass="Pad5Button" runat="server">
    <asp:Button runat="server" ID="btnSave" Text="Save" OnClick="btnSave_Click" CssClass="button"/>&nbsp;&nbsp;&nbsp;
    <asp:Button runat="server" ID="btnCancel" Text="Cancel" OnClick="btnCancel_Click" CausesValidation="False"/>
</flux:PadBox>
</flux:Section>

<%-- Validators --%>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" Runat="server" ControlToValidate="fChannelName" 
    ErrorMessage="You must specify the title of this channel" Display="None"/>

<asp:RequiredFieldValidator ID="RequiredFieldValidator2" runat="server" ControlToValidate="fChannelPath"
    ErrorMessage="You must specify the path of this channel" Display="None"/>

</flux:HostTable>

</form>
</body>
</html>

There are a large number of flux specific controls in there, but it should be reasonably self explanatory. The base class CMSContentForm within flux provides everything necessary to wire our form up. It invokes Save/Load content methods and utilises the form type code to determine what it's editing. All we do is override these methods and add the fields in/out of the persistence mechanism.

4. We need to go ahead and create a Page content type now also, so we can publish something so add another folder within types called Page along with the configuration file "Page.type.config" and it's editor form "form.aspx", using the following content:

/Settings/types/page/Page.type.config

<NodeType ID="Page" Name="Web Page" LimitVersions="1">

    <AddForm>~/settings/types/Page/form.aspx</AddForm>
    <EditForm>~/settings/types/Page/form.aspx</EditForm>

    <Relationships AddRoot="false">
        <Add>Page</Add>
    </Relationships>

    <StaticData>
        <Template>~/settings/types/Page/render.aspx</Template>
    </StaticData>

</NodeType>

/Settings/types/page/Form.aspx

<%@ Page Language="C#" Inherits="Deepcode.Flux.Core.UI.CMS.CMSContentForm" ValidateRequest="false"%>
<%@ Register TagPrefix="flux" Namespace="Deepcode.Flux.Core.UI.Controls" Assembly="Deepcode.Flux.Core"%>

<script runat="server">
    // Type ID being managed by this form
    protected override string FormTypeCode { get { return "Page"; } }

    // Do form setup
    protected override void SetupForm(int ContentID, int ParentID)
    {
        if (!Page.IsPostBack)
        {
            fPath.NodeIDParent = ParentID;
            fPath.NodeIDPathed = ContentID;
        }
    }

    // Save fields to content object
    protected override void SaveContent(ref Deepcode.Flux.Core.Systems.CMS.ContentObject save)
    {
        save.NodeName = this.fPath.Text;
        save.NodeTitle = this.fPageTitle.Text;
        save.Fields["MenuTitle"] = this.fMenuTitle.Text;
        save.Fields["BodyContent"] = this.fHtml.Text;
    }

    // Load fields from content object
    protected override void LoadContent(Deepcode.Flux.Core.Systems.CMS.ContentObject load)
    {
        this.fPath.Text = load.NodeName;
        this.fPageTitle.Text = load.NodeTitle;
        this.fMenuTitle.Text = load.Fields["MenuTitle"];
        this.fHtml.Text = load.Fields["BodyContent"];
    }
</script>

<html>
<head id="Head1" runat="server">
    <link href="../../../admin/Asset/Style/GeneralStyle.css" rel="Stylesheet" type="text/css" />
</head>
<body class="nopadshaded">
<form id="form1" runat="server">
<flux:HostTable ID="HostTable1" runat="server">

<%-- Summary --%>
<flux:ValidationSummarySection ID="ValidationSummarySection1" runat="server" HeaderText="Please correct the following errors"/>

<%-- Form area --%>
    <flux:Section ID="Section1" runat="server" Title="Add/Edit Page">
    <flux:ShadePadBox ID="ShadePadBox1" runat="server">

        <table cellspacing="0" cellpadding="3" border="0">
        <tr><td>Path:</td>
            <td><flux:NodePath runat="server" ID="fPath" Width="200px" MaxLength="100"/></td>
            </tr>

        <tr><td>Page Title:</td>
            <td><asp:TextBox runat="server" ID="fPageTitle" Width="300px" MaxLength="500"/></td>
            </tr>
        
        <tr><td>Title for menus:</td>
            <td><asp:TextBox runat="server" ID="fMenuTitle" Width="300px" MaxLength="50"/></td>
            </tr>
        </table>

    </flux:ShadePadBox>
    </flux:Section>
    
    <flux:Section ID="Section2" runat="server" Title="Page Content">
        <asp:TextBox runat="server" ID="fHtml" Width="100%" Height="150px" />
    </flux:Section>

<%-- Buttons --%>
<flux:Section ID="Section3" runat="server">
<flux:PadBox ID="PadBox1" CssClass="Pad5Button" runat="server">
    <asp:Button runat="server" ID="btnSave" Text="Save" OnClick="btnSave_Click" CssClass="button"/>&nbsp;&nbsp;&nbsp;
    <asp:Button runat="server" ID="btnCancel" Text="Cancel" OnClick="btnCancel_Click" CausesValidation="False"/>
</flux:PadBox>
</flux:Section>

<%-- Validators --%>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1" runat="server" ControlToValidate="fPath"
    ErrorMessage="You must specify the path for this page" Display="None"/>

<asp:RequiredFieldValidator ID="RequiredFieldValidator2" Runat="server" ControlToValidate="fPageTitle" 
    ErrorMessage="You must specify the title of this page" Display="None"/>


</flux:HostTable>

</form>
</body>
</html>

Step 7 - creating some content

image

1. Open the administration console (http://localhost/[youralias]/admin/) and select "Content Management" from the right hand menu.  You should be presented with an empty cms as follows;

image

2. Click "Add new" in the actions panel and you will be presented with the new "website channel" form which we defined earlier. This is presented immediately without asking what sort of content you wish to create as it's the only type that can be added to the root like this.

3. Enter test for the channel name and path as below and click save.

image

4. Now click the "test" node in the tree view to the left and then click "Add new". This will present the form for adding a page, again as defined above. Define a home page as follows;

image

5. Repeat the process to create another new page called "About us".

image

6. You should now have a site defined as follows;

image

7. And you will notice that all of your pages presently are shown with a green icon indicating they are checked out for editing by you.

image

8. In order for this content to appear on the site, it must be checked in, and optionally validated through the workflow processes. An easy way to check everything in quickly is to go back to the admin home page and select the items that are checked out to you and check them all in in one go. Alternatively, select each in turn and click check-in.

9. Once they are checked in they should present in blue;

image

Step 7a - using a WYSIWYG editor

A downside to what we've seen so far is our editor is free text. It doesn't allow us to write bold, italics, insert images etc. As such, we would normally obtain a WYSIWYG editor and embed that instead. In this instance, you could grab the latest version of FCKEditor, which is my favourite editor that works well across all browsers. Normally we'd download FCK editor's latest source and then apply flux specific changes to it, to allow the editor to use the standard flux asset and link browsers. Feel free to use this process, or to save time, you can download the basic starter kit which I am producing as a result of this article from our codeplex site. For those interested however, to use a WYSIWYG editor like this we would;

1. Download the latest FCKEditor and apply flux specific changes to it (replace the asset and link browser).

2. Extract it's contents to a folder in your site called /FCKEditor

3.  Copy the FredCK.FCKEditorV2.dll into your site's Bin directory.

4. Create a folder in settings called common and create a custom user control in there with the name fckEditor.ascx. The content of the ASCX should be as follows;

<%@ Control Language="C#" AutoEventWireup="true" %>
<%@ Register TagPrefix="fck" Namespace="FredCK.FCKeditorV2" Assembly="FredCK.FCKEditorV2" %>

<script runat="server">
    public string Value
    {
        get
        {
            return fContent.Value;
        }

        set
        {
            fContent.Value = value;
        }
    }
    
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!Page.IsPostBack)
        {
            fContent.BasePath = "~/FCKEditor/";
            fContent.ToolbarSet = "Default";
        }
    }
</script>

<fck:FCKeditor runat="server" ID="fContent" Height="200px"/>

5. We then replace the textbox in form.aspx for the Page content type;

6. From here, the editor should allow us to display html etc.

image

Step 8 - setting up mappings and wiring it up

Flux is capable of doing URL re-writing for you, and is capable of being multi-homed against different domains etc as a result. In order to tell flux which folder to get it's content from for different domains and which page is the home page, we need to use the mapping tool.

image

1. Open website mappings and select to add a new mapping

2. The domain we're adding a mapping for during development here is localhost, so enter that into the domain field.

3. Select the "test" root node as the site's content root node - this determines which node contains all the content for this domain.

4. Select the "home" node as the site's home page node.

image

5. Click save. You have successfully setup the mapping between the domain and the content.

image

6. As flux doesn't force you to use the URL re-writer engine, we must now hook this up in code. Create a default.aspx web form in the root of your site and set it's content as follows; This simply redirects to the correct URI whenever someone accesses the site.

<%@ Page Language="C#" %>
<%@ Import Namespace="Deepcode.Flux.Core.Systems.URLRewriter"%>

<script runat="server">
protected void Page_Load()
{
    Rewriter rw = new Rewriter();
    rw.RewriteFromHome( Context );
}
</script>

Step 9 - Writing the rendering template for the worlds most basic content type

With the domain maps etc in place, we're ready now to show the content we've created to the world. If you navigate now to your site, you will get an error saying it can't find the render template for the page. That's because we still need to create it.

1. Create a new web form - /settings/types/page/render.aspx and set it's content as follows;

<%@ Page Language="C#"%>
<%@ Import Namespace="Deepcode.Flux.Core.Systems.CMS" %>
<%@ Import Namespace="System.Linq" %>
<script runat="server">
    protected ContentObject Content { get; private set; }

    protected override void OnLoad(EventArgs e)
    {
        Content = ContentQuery.GetByID(Int32.Parse(Request["nodeid"]));
        if (Content == null) Response.Redirect("~/", true);
    
        Page.Title = Content.NodeTitle; 
        html.Text = Content.Fields["BodyContent"];
        
        // Get the menu together by querying everything within my parent....
        ContentQuery query = new ContentQuery();
        query.BaseMatch.AND(new StaticFieldMatch(
            ContentStaticField.ParentID, 
            ContentQueryOperator.EQ, 
            Content.FK_ParentID));
    
        menu.DataSource = from c in query.GetMatching() 
                          select new 
                          { 
                              Url = Page.ResolveUrl(String.Format("~/{0}/{1}.aspx", 
                                      c.NodePath, c.NodeName)), 
                              Title = c.NodeTitle };
        menu.DataBind();
        
        
        base.OnLoad(e);
    }
</script>

<html>
<head runat="server"/>
<body>

    <div>
    <asp:Repeater ID="menu" runat="server">
        <ItemTemplate>
            <a href="<%#Eval("Url")%>">[<%#Eval("Title")%>]</a> &middot;
        </ItemTemplate>
    </asp:Repeater>
    </div>

    <asp:Literal ID="html" runat="server"/>

</body>
</html>

You should now be able to navigate to your site and view a basic page with a simple menu:

image

image

Step 10 - What next?

Well, that was the overview for how to get a basic CMS up and running. It's quite complex, but this means that flux isn't prescriptive about anything - if you don't want to use it to define the actual structure of your site, you don't have to, you can use it as a content repository, and ask it for content as necessary. In fact this approach works quite well when building complex sites. You can even setup flux to work with the excellent ASP.NET MVC framework. Simply have one site for the admin and another site (the MVC app) for the front end, which in turn uses the flux API's to get at it's data.

Whilst this was an overview, I am intent now on packaging this as a sample onto codeplex (http://flux.codeplex.com) so that it can be used as a starting point very quickly without repeating all the steps above.

Notes on live deployment

When installing your finished product onto your deployment site, chances are you are installing to a shared environment where IIS is already configured for you and the database is already created, but empty. In this case, all you need to do to deploy the first time is;

  • FTP all of your files up to the server
  • On the first instance, delete settings/flux.config so that the database scripts are executed
  • Login to http://[yoursite.com]/admin/setup/install/ to walk through the installation process
  • Create your content and domain mappings.....

From then on, if you change your template code etc, you just need to re-deploy your files.