Currently I have a class that represents a document. This document needs to be displayed as HTML. I would like to have a method to call such as GetHTML()
that would then call GetHTML()
on any properties/sections of the document that needed to be rendered. I was initially thinking about using linq and XElement
but am wondering if that may cause issues with certain tags in HTML. Would I better off using an HtmlTextWriter
? I am open to any suggestio开发者_Go百科ns or best practives for this situation. Thanks!
I think you're talking about having a data Model that can render itself in HTML, and you want to make sure that there is fidelity in the composition and that it can recursively discover and render child objects.
Well, how "classic" ASP.net does this with its rendering model is that everything is a Control, and each control has a Render(HtmlTextWriter) method. Controls that contain child controls (composite controls, user controls, pages, etc) have logic in them to recursively render each of its child controls within the context of itself. E.g. The Render method for a composite controls calls RenderBeginTag, and then it calls RenderChildren and then it calls RenderEndTag, all the while passing around a reference to the same HtmlTextWriter that's being used to render the entire Response. Things like pages and user controls are even a little more complicated because they also parse templates (aspx), but at the end of the day those templates are just a prescription for a Render method.
Well, the trouble with this is that it sucks from a standpoint of separating concerns. Why does your data document need to contain all the code to render itself in HTML? I also don’t like the idea of GetHtml() because it implies (to me) that you’re going to reinvent something that has already been invented. Reusing an MVC ViewEngine here is probably a good idea: it has lots of features and is easy to develop against compared to something like the HtmlTextWriter.
At any rate. My suggestion would be to create a new interface for your documents classes; let’s call it “iRenderable.” And the single requirement for this interface is a string called “ViewName”.
When you want to convert your document (Model) to HTML, all you do is call RenderPartial(model.ViewName, model). If that document then has iRenderable child elements then (from within that view), you again call RenderPartial(model.SomeChild.ViewName, model.SomeChild).
There you go. MVC does the heavy lifting with the templates and what-not, and you don't bake a bunch of extra procedural rendering crap into your data model.
interface IRenderable
{
string ViewName
{
get;
}
}
public class Table : IRenderable
{
public string ViewName
{
get
{
return "Table";
}
}
public List<Row> Rows { get; set; }
}
public class Row : IRenderable
{
public string ViewName
{
get
{
return "Row";
}
}
public string Value { get; set; }
}
You might ask, why bother with the interface if Table knows that it contains Rows? Because there maybe be cases where you have an object (or a collection of objects) that you want to render and your parent view has no knowledge of the type. You would still be able to render the object by attempting to cast to IRenderable.
If you've created a DOM yourself, you might look into the Visitor pattern. It would allow you to avoid having specific code for creating HTML documents in your DOM itself. I've done the same a while ago and still find new ways of using this way of adding new functionality to my DOM classes. Here is some example:
interface IVisitable {
void Assign(IVisitor);
}
interface IVisitor {
void Visit(Document document);
void Visit(Section section);
void Visit(Paragraph paragraph);
}
class Element : IVisitable {
void IVisitable.Assign(IVisitor visitor) {
this.Assign(visitor);
}
protected abstract void Assign(IVisitor visitor);
}
abstract class Block : Element { }
abstract class Inline : Element { }
abstract class BlockContainer<TElement> : Block, ICollection<TElement> { }
abstract class ElementContainer<TElement> : Element, ICollection<TElement> { }
abstract class InlineContainer<TElement> : Inline, ICollection<TElement> { }
class Paragraph : BlockContainer<Inline> {
protected override void Assign(IVisitor visitor) { visitor.Visit(this); }
}
class Section : BlockContainer<Block> {
public string Title { get; set; }
protected override void Assign(IVisitor visitor) { visitor.Visit(this); }
}
class Document : ElementContainer<Block> {
protected override void Assign(IVisitor visitor) { visitor.Visit(this); }
}
This will give you a basic document structure, where you should add further elements, that derive from the 5 base classes I've provided. The Visitor-pattern highly relies on the fact, that the IVisitor interface knows all your implemented "visitable" elements. As you can see mine needed to know about the Document
, Section
and Paragraph
classes (as you might have guessed, it will contain much more in a real-world example).
For the part, where your elements have multiple childs, which should be rendered too, I've created two Visitor implementations:
abstract class Visitor : IVisitor {
public virtual void Visit(Document document) {
ICollection<Block> container = document; // I dont like to use the 'as' keyword, if I only want to fool the compiler to pick another overload.
this.Visit(container);
}
public virtual void Visit(Section section) {
ICollection<Block> container = section;
this.Visit(container);
}
public virtual void Visit(Paragraph paragraph) {
ICollection<Inline> container = paragraph;
this.Visit(container);
}
protected virtual void Visit(ICollection<Inline> container) { }
protected virtual void Visit(ICollection<Block> container) { }
protected virtual void Visit(ICollection<Element> container) { }
}
This one gives you an implementation where you can override only those elements you are interested in and if you don't override any of this methods are more generic Visit-method is introduced, which only targets the ICollection part of the elements (As I said before, the Visitor pattern requires you to know all about the visitable elements).
To create some depth-first search behavior with this implementation, another Visitor is introduced:
abstract class SearchVisitor : Visitor {
protected override Visit(ICollection<Block> container) {
this.AssignAll(container);
}
protected override Visit(ICollection<Inline> container) {
this.AssignAll(container);
}
protected override Visit(ICollection<Element> container) {
this.AssignAll(container);
}
private void AssignAll<TElement>(IEnumerable<TElement> container) {
foreach (IVisitable element in container) {
element.Assign(this);
}
}
}
If you want to generate HTML from your "Documents", you can then implement a SearchVisitor just for this purpose:
class HtmlMarkupRenderer : SearchVisitor {
private readonly XmlWriter writer;
private int sectionDepth = 0;
public HtmlMarkupRenderer(TextWriter textWriter) {
this.writer = XmlWriter.Create(textWriter, new XmlWriterSettings { ConformanceLevel = ConformanceLevel.Fragment });
public override void Visit(Section section) {
this.sectionDepth++;
string headingElement = String.Concat("h", Math.Max(this.sectionDepth, 6));
this.writer.WriteElementString(headingElement, section.Title);
// The base implementation will assign this visitor to all childs of section.
base.Visit(section);
this.sectionDepth--;
}
public override void Visit(Paragraph paragraph) {
this.writer.WriteStartElement("p");
base.Visit(paragraph);
this.writer.WriteEndElement();
}
}
I've spend a lot of time on this "layout", so I can reproduce it quite fast, but it might create some overhead. Let me know if you want more details on this one.
精彩评论