What is the best way to refactor presentation code out of my domain objects in an ASP.NET MVC solution?
See the question and my original answer on StackOverflowIt's not easy to provide a perfect answer to this issue. Although a total separation of layers is desirable, it often causes a lot useless engineering problems.
Although everyone is ok with the fact that the business layer must not know to much about the presentation/UI layer, I think it's acceptable for it to know these layers do exist, of course without too many details.
Once you have declared that, then you can use a very underused interface: IFormattable. This is the interface that string.Format uses.
So, for example, you could first define your Person class like this:
public class Person : IFormattable
{
public string Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
// reroute standard method to IFormattable one
return ToString(null, null);
}
public virtual string ToString(string format, IFormatProvider formatProvider)
{
if (format == null)
return Name;
if (format == "I")
return Id;
// note WebUtility is now defined in System.Net so you don't need a reference on "web" oriented assemblies
if (format == "A")
return string.Format(formatProvider, "<a href='/People/Detail/{0}'>{1}</a>", WebUtility.UrlEncode(Id), WebUtility.HtmlDecode(Name));
// implement other smart formats
return Name;
}
}
This is not perfect, but at least, you'll be able to avoid defining hundreds of specified properties and keep the presentation details in a ToString method that was meant speficially for presentation details.
From calling code, you would use it like this:
string.Format("{0:A}", myPerson);
or use MVC's HtmlHelper.FormatValue. There are a lot of classes in .NET that support IFormattable (like StringBuilder for example).
You can refine the system, and do this instead:
public virtual string ToString(string format, IFormatProvider formatProvider)
{
...
if (format.StartsWith("A"))
{
string url = format.Substring(1);
return string.Format(formatProvider, "<a href='{0}{1}'>{2}</a>", url, WebUtility.UrlEncode(Id), WebUtility.HtmlDecode(Name));
}
...
return Name;
}
You would use it like this:
string.Format("{0:A/People/Detail/}", person)
So you don't hardcode the url in the business layer. With the web as a presentation layer, you'll usually have to pass a CSS class name in the format to avoid hardcoding style in the business layer. In fact, you can come up with quite sophisticated formats. After all, this is what's done with objects such as DateTime if you think about it.
You can even go further and use some ambiant/static property that tells you if you're running in a web context so it works automatically, like this:
public class Address : IFormattable
{
public string Recipient { get; set; }
public string Line1 { get; set; }
public string Line2 { get; set; }
public string ZipCode { get; set; }
public string City { get; set; }
public string Country { get; set; }
....
public virtual string ToString(string format, IFormatProvider formatProvider)
{
// http://stackoverflow.com/questions/3179716/how-determine-if-application-is-web-application
if ((format == null && InWebContext) || format == "H")
return string.Join("<br/>", Recipient, Line1, Line2, ZipCode + " " + City, Country);
return string.Join(Environment.NewLine, Recipient, Line1, Line2, ZipCode + " " + City, Country);
}
}