I need a simple functionality for adding pages/changing content on those pages. I've looked at n2, and other pre build CMS tools but these are way to advanced for a simple CMS functionality that I need.
What's the best approach? I already have an MVC app that I would like to add/build a simple features like:
not sure where to start.
any info greatly appreciated. thanks
this is for .NET MVC
Assuming you're using ASP.NET MVC and you want to keep it simple, how about something like this:
public abstract class TemplateBase
{
public abstract string TemplateName { get; }
}
public class SingleColumnTemplate : TemplateBase
{
public override string TemplateName { get { return "Single-column page"; } }
public AreaContainer CenterColumn { get; protected set; }
public SingleColumnTemplate()
{
CenterColumn = new AreaContainer("Center column");
}
}
public class TwoColumnTemplate : TemplateBase
{
public override string TemplateName { get { return "Two-column page"; } }
public AreaContainer LeftColumn { get; protected set; }
public AreaContainer RightColumn { get; protected set; }
public TwoColumnTemplate()
{
LeftColumn = new AreaContainer("Left column");
RightColumn = new AreaContainer("Right column");
}
}
// TODO Add more template types
public class AreaContainer
{
public string ContainerName { get; set; }
public IList<AreaBase> Areas { get; protected set; }
public AreaContainer(string name)
{
ContainerName = name;
Areas = new List<AreaBase>();
}
}
public abstract class AreaBase
{
public abstract string AreaName { get; }
}
public class HtmlArea : AreaBase
{
public override string AreaName { get { return "HTML content"; } }
public string HtmlContent { get; set; }
}
// TODO Add more area types
public class Page
{
public int Id { get; set; }
public string Title { get; set; }
public TemplateBase Template { get; set; }
}
The controller action for editing an existing page could look something like:
public class PageAdminController : Controller
{
[HttpGet]
ActionResult Edit(int id)
{
var page = GetPageFromStorageById(id);
// TODO If the page is not found, issue 404
return View(page);
}
// ...
}
In the view (Views/PageAdmin/Edit.aspx
), which should be strongly typed to ViewPage<Page>
, you can use the HtmlHelper.EditorFor(...)
method to render the appropriate template view, provided you have created a partial view for each template type:
<!-- Inside the edit view for Page (Edit.aspx) -->
<%: Html.HiddenFor(m => m.Id) %>
<%: Html.EditorFor(m => m.Title) %>
<%: Html.EditorFor(m => m.Template) %>
In the folder Views/PageAdmin/EditorTemplates
you would then place partial edit views for each template and area type (i.e. SingleColumnTemplate.ascx
, TwoColumnTemplate.ascx
and HtmlArea.ascx
). You would probably also want to create a partial view for AreaContainer
.
As for the controller action that receives the edited page, things get a little bit more complicated. Since Page
has a property of type TemplateBase
, which is an abstract class, the DefaultModelBinder
won't know how to populate it. You can get around this by writing a custom model binder that somehow "knows" which implementing class to instantiate. And how would it know that? One option that I can think of is to include a hidden field in the view that contains the name of the actual runtime type of the page template. It's a bit of a hack, I guess, but since you are after simplicity I think it would be allright. In that case, just include a property called, for example, RuntimeTypeName
in the TemplateBase
class:
public string RuntimeTypeName { get { return GetType().FullName; } }
Since it just calls GetType()
, which is a virtual method overridden by all types by default, it will return the name of the runtime template type.
Then you must make sure that the partial views you created for your TemplateBase
implementations include a (hidden) field for the TemplateBase.RuntimeTypeName
property. In other words, in SingleColumnTemplate.ascx
and TwoColumnTemplate.ascx
you would have this line:
<%: Html.HiddenFor(m => m.RuntimeTypeName) %>
A model binder that utilizes this information to create the right type of template could look like this:
/// <summary>
/// Model binder hack that builds upon the DefaultModelBinder,
/// but that can detect the "proper" subclass/implementing class
/// type for a model, assuming the name of that type is contained
/// in a field called "RuntimeTypeName".
/// </summary>
public class InheritanceSupportingModelBinder : DefaultModelBinder
{
// Assume that the name of the field that contains the
// runtime type name is called "RuntimeTypeName"
public const string RuntimeTypeNameField = "RuntimeTypeName";
private Type RuntimeType { get; set; }
// This method is called by the DefaultModelBinder to find out which
// properties of the current model that it should attempt to bind
protected override PropertyDescriptorCollection GetModelProperties(
ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// If we have found out the runtime type of the model through
// looking at the "special" field above, use the properties of that type.
// Otherwise, use the default behavior.
if (RuntimeType != null)
{
return TypeDescriptor.GetProperties(RuntimeType);
}
else
{
return base.GetModelProperties(controllerContext, bindingContext);
}
}
// This method is called by the DefaultModelBinder when it
// tries to create an instance of the model class. If the
// class is abstract, an exception will be thrown. Therefore
// we try to read the name of the actual type from the
// RuntimeTypeName (hidden) field and return an instance of that type.
protected override object CreateModel(ControllerContext controllerContext,
ModelBindingContext bindingContext,
Type modelType)
{
if (bindingContext.ValueProvider.ContainsPrefix(
bindingContext.ModelName + "." + RuntimeTypeNameField))
{
var result = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName + "." + RuntimeTypeNameField);
if (result != null && !string.IsNullOrEmpty(result.AttemptedValue))
{
// Check that the type indicated by the hidden field is really
// a subclass of (or implementing) the indicated base class
var tempType = Type.GetType(result.AttemptedValue);
if (modelType.IsAssignableFrom(tempType))
{
RuntimeType = modelType = tempType;
}
}
}
return base.CreateModel(controllerContext, bindingContext, modelType);
}
}
Disclaimer: I'm a beginner at ASP.NET MVC myself, so this model binder might well be faulty. I've put it together by looking a bit at the source code for DefaultModelBinder
and by trial-and-error. It's just an example, but according to my (quick and dirty) testing it seems to work.
Of course you need to register it in the Global.asax for it to kick in:
ModelBinders.Binders.Add(
typeof(TemplateBase),
new InheritanceSupportingModelBinder());
But we're not done! Remember that the AreaContainer.Areas
collection is of type IList<AreaBase>
- and since AreaBase
is also an abstract class, we have to apply the same hack for it to be bound correctly. That is, add the RuntimeTypeName
property to the AreaBase
class and register our custom model binder for the AreaBase
class in Global.asax
.
Provided we have followed all these steps so far, we could have an action method on our PageAdminController
for handling edits of pages that looks something like this:
[HttpPost]
public ActionResult Edit(Page page)
{
if (!ModelState.IsValid)
{
return View(page);
}
// TODO Save page to database or whatever
// TODO Redirect to page index
}
The action methods for creating a new page is left as an exercise, it shouldn't be that difficult (user selects template from a list, the proper form is displayed, post-handling action like above).
Displaying pages should be trivial, just use the HtmlHelper.DisplayFor(...)
instead of EditorFor(...)
, create the corresponding partial views and you're set.
For WYSIWYG editing of content, you probably would want to use a third-party component. CKEditor, TinyMCE, YUI Rich Text Editor and Telerik Editor are some examples.
That was my take on this! All comments are welcome; as I mentioned I'm learning ASP.NET MVC myself and it would be great if my mistakes were pointed out by people who know better.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With