Is it possible to write out a generated ordered sequence of numbers, spanning a content page and a master page with MVC/Razor?
The following example code is a simplified version of a setup we use for ads on our pages where we have to write a "position" number on each ad tag. The position numbers must be sequential from top to bottom starting with 1. The problem is that some of the ad tags are defined in the master/layout page and others are defined within the page markup.
Please see the following example code:
_Layout.cshtml:
<!DOCTYPE html>
<html>开发者_如何学Go;
<head><title>Ad Position Test</title></head>
<body>
@Html.SequentialNumber()
@RenderBody()
@Html.SequentialNumber()
</body>
</html>
Index1.cshtml:
@{Layout = "~/Views/Shared/_Layout.cshtml";}
@Html.SequentialNumber()
Index2.cshtml:
@{Layout = "~/Views/Shared/_Layout.cshtml";}
@Html.SequentialNumber()
@Html.SequentialNumber()
Helpers.cs
public static class HtmlHelperExtensions
{
public static HtmlString SequentialNumber(this HtmlHelper html)
{
var tile = (int)(html.ViewData["Tile"] ?? 1);
html.ViewData["Tile"] = tile + 1;
return new HtmlString(tile.ToString());
}
}
The output of this setup for Index1.cshtml is: "2 1 3" and for Index2.cshtml it is: "3 1 2 4". This is of course the result of the RenderBody method being executed before the master content is executed.
So question is, how do you output a correctly ordered sequence of numbers that spans both the master page and the content page such that the output will be "1 2 3" and "1 2 3 4" respectively?
You can achieve this through the cunning use of action filters*
* I would like to preface my answer with the fact that I can't necessarily recommend you actually use the method I am about to post. But I was curious whether this could be done, so I did. I've discussed a more reasonable solution using managed HTTP handlers in IIS7 in the comments below, see also this awesome tutorial.
Because, as you have discovered, nested views are rendered in their nesting order, you can't achieve the required effect during the normal view rendering phase. But after the view has been rendered, there's nothing stopping us from modifying the resulting markup (except perhaps common sense).
So first, let's change the HTML helper to emit some kind of marker we can replace:
public static class HtmlHelperExtensions
{
public static HtmlString SequentialNumber(this HtmlHelper html)
{
//Any sufficiently unique string would do
return ":{ad_sequence}";
}
}
In ASP.NET MVC you can decorate controller classes and controller actions with action filter attributes to execute code before and after the actions have been executed. For example, here we will define an action filter to handle all methods inside the HomeController
, and only the Index()
action, respectively:
[AdSequencePostProcessingFilter]
public class HomeController : Controller
{
}
public class HomeController : Controller
{
[AdSequencePostProcessingFilter]
public ActionResult Index()
{
return View();
}
}
In ASP.NET MVC 3 we can also have global filters which apply to all controller actions in your application:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalFilters.Filters.Add(new AdSequencePostProcessingFilterAttribute());
}
}
You can decide to apply the filter on individual actions, controllers or globally. Your choice.
Now we'll need to define the AdSequencePostProcessingFilterAttribute
filter (based on this caching filter class):
public class AdSequencePostProcessingFilterAttribute : ActionFilterAttribute
{
private Stream _output;
private const string AdSequenceMarker = ":{ad_sequence}";
private const char AdSequenceStart = ':';
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//Capture the original output stream;
_output = filterContext.HttpContext.Response.Filter;
filterContext.HttpContext.Response.Flush();
filterContext.HttpContext.Response.Filter = new CapturingResponseFilter(filterContext.HttpContext.Response.Filter);
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
//Get the emitted markup
filterContext.HttpContext.Response.Flush();
CapturingResponseFilter filter =
(CapturingResponseFilter)filterContext.HttpContext.Response.Filter;
filterContext.HttpContext.Response.Filter = _output;
string html = filter.GetContents(filterContext.HttpContext.Response.ContentEncoding);
//Replace the marker string in the markup with incrementing integer
int adSequenceCounter = 1;
StringBuilder output = new StringBuilder();
for (int i = 0; i < html.Length; i++)
{
char c = html[i];
if (c == AdSequenceStart && html.Substring(i, AdSequenceMarker.Length) == AdSequenceMarker)
{
output.Append(adSequenceCounter++);
i += (AdSequenceMarker.Length - 1);
}
else
{
output.Append(c);
}
}
//Write the rewritten markup to the output stream
filterContext.HttpContext.Response.Write(output.ToString());
filterContext.HttpContext.Response.Flush();
}
}
We'll also need a sink where we can capture the output:
class CapturingResponseFilter : Stream
{
private Stream _sink;
private MemoryStream mem;
public CapturingResponseFilter(Stream sink)
{
_sink = sink;
mem = new MemoryStream();
}
// The following members of Stream must be overriden.
public override bool CanRead { get { return true; } }
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return false; } }
public override long Length { get { return 0; } }
public override long Position { get; set; }
public override long Seek(long offset, SeekOrigin direction)
{
return 0;
}
public override void SetLength(long length)
{
_sink.SetLength(length);
}
public override void Close()
{
_sink.Close();
mem.Close();
}
public override void Flush()
{
_sink.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return _sink.Read(buffer, offset, count);
}
// Override the Write method to filter Response to a file.
public override void Write(byte[] buffer, int offset, int count)
{
//Here we will not write to the sink b/c we want to capture
//Write out the response to the file.
mem.Write(buffer, 0, count);
}
public string GetContents(Encoding enc)
{
var buffer = new byte[mem.Length];
mem.Position = 0;
mem.Read(buffer, 0, buffer.Length);
return enc.GetString(buffer, 0, buffer.Length);
}
}
And voilà, we have the sequence increading in document order :P
You could force the numbers on the SequentialNumber() function that comes before the RenderBody and then start the sequence from there. I guess there is a more generic solution, but cant think of any right now :P
Edit What about this:
public static class HtmlHelperExtensions
{
public static HtmlString SequentialNumber(this HtmlHelper html,int? sequence)
{
if(sequence!=null)
{
var tile = sequence;
html.ViewData["Tile"] = sequence + 1;
}
else
{
var tile = (int)(html.ViewData["Tile"] ?? 1);
html.ViewData["Tile"] = tile + 1;
}
return new HtmlString(tile.ToString());
}
}
And then
<!DOCTYPE html>
<html>
<head><title>Ad Position Test</title></head>
<body>
@Html.SequentialNumber(1)
@Html.SequentialNumber(2)
@RenderBody()
@Html.SequentialNumber(null)
</body>
@{Layout = "~/Views/Shared/_Layout.cshtml";}
@Html.SequentialNumber(null)
@Html.SequentialNumber(null)
I'm not sure if you can be more generic, somehow you have to tell your helper that one call have more priority that the other.
精彩评论