构建MVC CMS

发布于 2024-10-01 03:10:12 字数 262 浏览 6 评论 0原文

我需要一个简单的功能来添加页面/更改这些页面上的内容。我研究过 n2 和其他预构建 CMS 工具,但这些是我需要的简单 CMS 功能的高级方法。

最好的方法是什么?我已经有一个 MVC 应用程序,我想添加/构建一个简单的功能,例如:

  1. 指定模板,
  2. 向该模板添加区域,
  3. 通过所见即所得添加内容。

不知道从哪里开始。

任何信息都非常感谢。 谢谢,

这是 .NET MVC 的

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:

  1. specify a template
  2. add areas to that template
  3. add content via wysiwyg.

not sure where to start.

any info greatly appreciated.
thanks

this is for .NET MVC

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

优雅的叶子 2024-10-08 03:10:12

假设您正在使用 ASP.NET MVC 并且希望保持简单,那么像这样的操作怎么样:

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; }
}

用于编辑现有页面的控制器操作可能类似于:

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);
    }

    // ...
}

在视图中 (Views/PageAdmin/Edit.aspx),应强类型化为 ViewPage,您可以使用 HtmlHelper.EditorFor(...) 方法来呈现适当的模板视图,前提是您已为每个模板类型创建了部分视图:

<!-- Inside the edit view for Page (Edit.aspx) -->
<%: Html.HiddenFor(m => m.Id) %>
<%: Html.EditorFor(m => m.Title) %>
<%: Html.EditorFor(m => m.Template) %>

在文件夹 Views/PageAdmin/EditorTemplates 中,您可以为每个模板和区域类型放置部分编辑视图(即 SingleColumnTemplate.ascx< /code>、TwoColumnTemplate.ascxHtmlArea.ascx)。您可能还想为 AreaContainer 创建一个部分视图。

至于接收编辑页面的控制器操作,事情变得有点复杂。由于 Page 具有 TemplateBase 类型的属性(这是一个抽象类),因此 DefaultModelBinder 将不知道如何填充它。您可以通过编写一个自定义模型绑定器来解决这个问题,该模型绑定器以某种方式“知道”要实例化哪个实现类。它怎么知道这一点?我能想到的一种选择是在视图中包含一个隐藏字段,其中包含页面模板的实际运行时类型的名称。我想这有点麻烦,但既然你追求简单性,我认为这应该没问题。在这种情况下,只需在 TemplateBase 类中包含一个名为 RuntimeTypeName 的属性即可:

public string RuntimeTypeName { get { return GetType().FullName; } }

因为它只调用 GetType(),即默认情况下被所有类型覆盖的虚拟方法,它将返回运行时模板类型的名称。

然后,您必须确保为 TemplateBase 实现创建的分部视图包含 TemplateBase.RuntimeTypeName 属性的(隐藏)字段。换句话说,在 SingleColumnTemplate.ascxTwoColumnTemplate.ascx 中,您将看到这一行:

<%: Html.HiddenFor(m => m.RuntimeTypeName) %>

利用此信息创建正确类型模板的模型绑定器可能如下所示:

/// <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);
    }
}

免责声明:我自己是 ASP.NET MVC 的初学者,因此这个模型绑定器很可能有问题。我通过查看 DefaultModelBinder 的源代码并通过反复试验将其组合在一起。这只是一个例子,但根据我的(快速而肮脏的)测试,它似乎有效。

当然,您需要在 Global.asax 中注册它才能启动:

ModelBinders.Binders.Add(
    typeof(TemplateBase), 
    new InheritanceSupportingModelBinder());

但我们还没有完成!请记住,AreaContainer.Areas 集合的类型为 IList- 并且由于 AreaBase 也是一个抽象类,因此我们必须应用正确绑定它的方法相同。也就是说,将 RuntimeTypeName 属性添加到 AreaBase 类,并在 Global.asax< 中为 AreaBase 类注册我们的自定义模型绑定器。 /代码>。

如果到目前为止我们已经遵循了所有这些步骤,我们就可以在 PageAdminController 上有一个用于处理页面编辑的操作方法,如下所示:

[HttpPost]
public ActionResult Edit(Page page)
{
    if (!ModelState.IsValid)
    {
        return View(page);
    }
    // TODO Save page to database or whatever
    // TODO Redirect to page index
}

用于创建新页面的操作方法留作练习,它不应该那么困难(用户从列表中选择模板,显示正确的表单,像上面那样的后处理操作)。

显示页面应该很简单,只需使用 HtmlHelper.DisplayFor(...) 而不是 EditorFor(...),创建相应的部分视图即可。

对于所见即所得的内容编辑,您可能需要使用第三方组件。 CKEditorTinyMCEYUI 富文本编辑器Telerik 编辑器 a> 是一些示例。

这就是我对此的看法!欢迎大家提出意见;正如我提到的,我正在自己学习 ASP.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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文