patterncsharpMinor
Html rendering framework with dynamics
Viewed 0 times
withdynamicsrenderinghtmlframework
Problem
I came to the conclusion that the previous framework was too complicated and not easily extendable so I tried it again but this time with dynamics.
It's much much shorter and I think it's much easier to extend it now. Now it's also much easier to write another renderer that creates i.e. a XDocument/XElement.
Core
The main part of the new framework is the
```
public class MarkupBuilder : DynamicObject, IEnumerable
{
private readonly List _extensions = new List();
private MarkupBuilder(MarkupBuilder markupBuilder, string tag)
{
_extensions = markupBuilder._extensions;
MarkupSchema = new MarkupSchema(markupBuilder.MarkupSchema);
Tag = tag;
Attributes = new List();
Content = new List();
}
public MarkupBuilder(MarkupSchema markupSchema = null)
{
MarkupSchema = new MarkupSchema(markupSchema ?? new MarkupSchema());
Attributes = new List();
Content = new List();
}
public IEnumerable Extensions => _extensions.AsReadOnly();
public MarkupSchema MarkupSchema { get; }
public string Tag { get; }
public List Attributes { get; }
public List Content { get; }
public MarkupBuilder Parent { get; private set; }
private int Depth
{
get
{
var depth = 0;
var parent = Parent;
while (parent != null)
{
depth++;
parent = parent.Parent;
}
return depth;
}
}
public MarkupBuilder Register() where T : IMarkupBuilderExtension, new()
{
_extensions.Add(new T());
return this;
}
// supports object initializer
public void Add(object content)
{
if (content != null)
{
Content.Add(content);
var htmlElement = content as MarkupBuilder;
It's much much shorter and I think it's much easier to extend it now. Now it's also much easier to write another renderer that creates i.e. a XDocument/XElement.
Core
The main part of the new framework is the
MarkupBuilder that is derived from the DynamicObject. It provides all the basic functionality for creating elements.```
public class MarkupBuilder : DynamicObject, IEnumerable
{
private readonly List _extensions = new List();
private MarkupBuilder(MarkupBuilder markupBuilder, string tag)
{
_extensions = markupBuilder._extensions;
MarkupSchema = new MarkupSchema(markupBuilder.MarkupSchema);
Tag = tag;
Attributes = new List();
Content = new List();
}
public MarkupBuilder(MarkupSchema markupSchema = null)
{
MarkupSchema = new MarkupSchema(markupSchema ?? new MarkupSchema());
Attributes = new List();
Content = new List();
}
public IEnumerable Extensions => _extensions.AsReadOnly();
public MarkupSchema MarkupSchema { get; }
public string Tag { get; }
public List Attributes { get; }
public List Content { get; }
public MarkupBuilder Parent { get; private set; }
private int Depth
{
get
{
var depth = 0;
var parent = Parent;
while (parent != null)
{
depth++;
parent = parent.Parent;
}
return depth;
}
}
public MarkupBuilder Register() where T : IMarkupBuilderExtension, new()
{
_extensions.Add(new T());
return this;
}
// supports object initializer
public void Add(object content)
{
if (content != null)
{
Content.Add(content);
var htmlElement = content as MarkupBuilder;
Solution
This looks really weird:
what is the point of casting
As I'm sure you know, classes' and methods' names should start with capital letter and blah blah as per C# naming convention. Why do you insist on using camel case?
You should make your api more consistent:
either return
As for your design, my main concern is that
You know, similar to how
P.S. Personally, I find dynamic objects useful as part of internal implementation, for which you write unit tests and leave it alone. I would not use dynamics as part of public API. Yes the code is shorter. But at the same time it is more complex and is more error prone. It's really easy to get dynamic objects wrong, and there is no compiler to tell you that "hey, this,
public static string RenderMarkup(MarkupBuilder builder)
{
return RenderMarkup(builder, builder.MarkupSchema);
}
private static string RenderMarkup(object value, MarkupSchema markupSchema)
{
var markupBuilder = value as MarkupBuilder;
if (markupBuilder == null)
{
return value == null ? string.Empty : (string)value;
}what is the point of casting
MarkupBuilder to object and then casting it back?As I'm sure you know, classes' and methods' names should start with capital letter and blah blah as per C# naming convention. Why do you insist on using camel case?
You should make your api more consistent:
public void Add(object content)
public MarkupBuilder AddRange(params object[] content)either return
MarkupBuilder from both methods, or make both methods void.As for your design, my main concern is that
MarkupBuilder appears to have two distinct responsibilities. First - constructing an html element using extensions and dynamic methods. Second - holding the actual result of construction: a strongly typed data. I think you should have two separate classes for that. So the usage would look like this:var body =
//describe the markup
html.body(html.p("foo"))
//return strongly typed non-dynamic and, possibly, immutable object, that holds actual data
.Build();You know, similar to how
StringBuilder works, where you call ToString() once you are done, and get rid of all the extra overhead.P.S. Personally, I find dynamic objects useful as part of internal implementation, for which you write unit tests and leave it alone. I would not use dynamics as part of public API. Yes the code is shorter. But at the same time it is more complex and is more error prone. It's really easy to get dynamic objects wrong, and there is no compiler to tell you that "hey, this,
html.spun method you have just called, looks suspicions, did you mean span?" So i would rather implement a bunch of extra classes once (as you did in your previous implementation), than hunt dynamic-related bugs for the rest of my life. :)Code Snippets
public static string RenderMarkup(MarkupBuilder builder)
{
return RenderMarkup(builder, builder.MarkupSchema);
}
private static string RenderMarkup(object value, MarkupSchema markupSchema)
{
var markupBuilder = value as MarkupBuilder;
if (markupBuilder == null)
{
return value == null ? string.Empty : (string)value;
}public void Add(object content)
public MarkupBuilder AddRange(params object[] content)var body =
//describe the markup
html.body(html.p("foo"))
//return strongly typed non-dynamic and, possibly, immutable object, that holds actual data
.Build();Context
StackExchange Code Review Q#136317, answer score: 5
Revisions (0)
No revisions yet.