Pergunta

Ok, I'm cruising through with ILSpy and trying to sort out what's going on here, but I'm not having much luck.

In a given view in ASP.NET MVC4 application (though, surely this is applicable to MVC3, possibly 2) that uses the Razor engine, the OutputStack property inherited from WebPageBase is simply a stack of TextWriter objects.

By pushing and popping from this stack, one can manipulate the output of a given Razor view. I was hoping to leverage this with HTML helper extension methods, however I also noticed that ViewContext, which is also a public member of HtmlHelper has it's own TextWriter.

Now, I ran a quick check: (from within a view)

@object.ReferenceEquals(this.ViewContext.Writer, this.OutputStack.Peek()) // True

However, the following confuses me to all hell:

@{
    // push one on
    OutputStack.Push(new StringWriter());
}
Hello!
@{
    // capture everything and pop it
    string buffer1 = OutputStack.Peek().ToString();
    OutputStack.Pop();
}
<pre>@(buffer1)</pre>

@{
    // apparently it references the top of the OutputStack
    // so this should be essentially the same
    var oldWriter = ViewContext.Writer;
    ViewContext.Writer = new StringWriter();
}
World!
@{
    // revert it and hope for the best
    string buffer2 = ViewContext.Writer.ToString();
    ViewContext.Writer = oldWriter;
}
<pre>@(buffer2)</pre>

The results from the above are as follows:

  • Hello! is captured into buffer1, and actually dumped after as <pre>Hello!</pre>. This is both expected and desired.
  • World! on the other hand, is output immediately, followed by an empty <pre></pre> block; in other words, it's not capturing anything.

My question is as follows: How do these TextWriter objects relate to one another, and why can I not manage the reference through ViewContext the same as I can from the top of the OutputStack? (rather, how can I manage the reference through ViewContext?)


Addendum
Details and other crap as I come across it.

  • The Writer property of ViewContext doesn't discard values passed to the setter, so it's not just dropping it in the case of the second example.
  • Moving along the call stack, OutputStack is actually coming from PageContext.
  • ViewContext.Writer is set in WebViewPage.ExecutePageHierarchy() using the WebViewPage property Output, which is the top of the OutputStack! (is it just me, or is this beginning to look like a rat's nest of dependencies?)
Foi útil?

Solução

I am posting my comment as an answer. It might note solve the academic part of your question but it might definitely solve the practical part of the question and most importantly the initial problem you were trying to solve.

In your helper you could get the OutputStack of the current view:

public static void MyHelper(this HtmlHelper html)
{
    var stack = ((WebPageBase)html.ViewDataContainer).OutputStack;

    ... you could push and pop here and solve your real world problem
}

Outras dicas

As per my comment above, it appears you need to manually keep OutputStack.Peek() and ViewContext.Writer in sync. The following helper method will do that:

    private class WriterScope : IDisposable
    {
        private HtmlHelper _html;
        private TextWriter _previous;

        public WriterScope(HtmlHelper html, TextWriter writer)
        {
            _html = html;
            _previous = _html.ViewContext.Writer;
            _html.ViewContext.Writer = writer;
            ((WebPageBase)_html.ViewDataContainer).OutputStack.Push(writer);
        }

        public void Dispose()
        {
            var stack = ((WebPageBase) _html.ViewDataContainer).OutputStack;
            if (stack.Peek() == _html.ViewContext.Writer)
                _html.ViewContext.Writer = _previous;
            stack.Pop();
            _html = null;
            _previous = null;
        }
    }

    public static IDisposable Scope(this HtmlHelper html, TextWriter writer)
    {
        return new WriterScope(html, writer);
    }

In the view it can be used like this:

@{
    var sw = new StringWriter();
    using (Html.Scope(sw))
    {
        <p>Line 2</p>
        <p>@Html.Raw("Line 3")</p>
        <p>Line 4</p>
    }
}
<p>Line 1</p>
@Html.Raw(sw.ToString())

The page outputs the following as expected:

Line 1
Line 2
Line 3
Line 4
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top