Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@Html.Partial fails always #1

Open
RobSchoenaker opened this issue Sep 28, 2017 · 16 comments
Open

@Html.Partial fails always #1

RobSchoenaker opened this issue Sep 28, 2017 · 16 comments

Comments

@RobSchoenaker
Copy link

Trying to use the code in an existing project that uses @html extensions.
The issue that arises is that the compilation always fails with these errors:

error CS0103: The name 'Html' does not exist in the current context

Even isolating the test and adding @Html.Raw('test') to the Index.cshtml produces this result.
Any thoughts?

image

@guardrex
Copy link
Owner

I think I might need to do all of the Razor imports for the custom template engine ...

private static RazorSourceDocument GetDefaultImports()
{
    using (var stream = new MemoryStream())
    using (var writer = new StreamWriter(stream, Encoding.UTF8))
    {
        writer.WriteLine("@using System");
        writer.WriteLine("@using System.Collections.Generic");
        writer.WriteLine("@using System.Linq");
        writer.WriteLine("@using System.Threading.Tasks");
        writer.WriteLine("@using Microsoft.AspNetCore.Mvc");
        writer.WriteLine("@using Microsoft.AspNetCore.Mvc.Rendering");
        writer.WriteLine("@using Microsoft.AspNetCore.Mvc.ViewFeatures");
        writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<TModel> Html");
        writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json");
        writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component");
        writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.IUrlHelper Url");
        writer.WriteLine("@inject global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider");
        writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor");
        writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.HeadTagHelper, Microsoft.AspNetCore.Mvc.Razor");
        writer.WriteLine("@addTagHelper Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper, Microsoft.AspNetCore.Mvc.Razor");
        writer.Flush();

        stream.Position = 0;
        return RazorSourceDocument.ReadFrom(stream, fileName: null, encoding: Encoding.UTF8);
    }
}

... and then specify those defaults ...

public CustomRazorTemplateEngine2(RazorEngine engine, RazorProject project) 
    : base(engine, project)
{
    Options.ImportsFileName = "_ViewImports.cshtml";
    Options.DefaultImports = GetDefaultImports();
}

However, I just moved a test app to 2.1, and it's breaking now. It looks like the new <body> Tag Helper must have a closing tag, but WebMarkupMin.Core removes that as part of its minification. Therefore, I'll probably need to escape the closing <body> tag.

I'm slammed at the moment, but I'll try to get back to this by Sunday and ping you back here. In the meantime, try to implement those defaults ☝️ and see if that works.

@guardrex
Copy link
Owner

guardrex commented Sep 29, 2017

Yes, I was able to confirm that stopping WebMarkupMin.Core from dropping the closing <body> tag did the trick ...

<!--wmm:ignore--></body><!--/wmm:ignore-->

..... and the way to fix it is to have the minifier always close tags. Setup the HtmlMinifier like this ...

private HtmlMinifier _htmlMinifier = new HtmlMinifier(new HtmlMinificationSettings() {RemoveOptionalEndTags = false});

I'll take a look at the Html.Raw situation and update the blog post asap. Thanks for bringing this to my attention.

@RobSchoenaker
Copy link
Author

RobSchoenaker commented Sep 29, 2017

There is another issue, I am looking into now. The partial does not have a DOCTYPE, so substrinning fails.

Fix:

public override RazorCodeDocument CreateCodeDocument(RazorProjectItem projectItem)
{
  if (projectItem == null)
  {
    throw new ArgumentNullException($"{nameof(projectItem)} is null!");
  }

  if (!projectItem.Exists)
  {
    throw new InvalidOperationException($"{nameof(projectItem)} doesn't exist!");
  }

  Console.WriteLine();
  Console.WriteLine($"File: {projectItem.FileName}");

  using (var inputStream = projectItem.Read())
  {
    using (var reader = new StreamReader(inputStream))
    {
      var text = reader.ReadToEnd();

      var markup = string.Empty;
      var directives = string.Empty;
      var markupStart = text.IndexOf("<!DOCTYPE");
      if (markupStart != -1)
      {
        directives = text.Substring(0, markupStart);
        markup = text.Substring(markupStart);
      }
      else
      {
        markup = text;
      }
      text = directives + Minify(markup);

      var byteArray = Encoding.UTF8.GetBytes(text);
      var minifiedInputStream = new MemoryStream(byteArray);

      var source = RazorSourceDocument.ReadFrom(minifiedInputStream, projectItem.PhysicalPath);
      var imports = GetImports(projectItem);

      return RazorCodeDocument.Create(source, imports);
    }
  }
}

@RobSchoenaker
Copy link
Author

RobSchoenaker commented Sep 29, 2017

Almost working on my project, but now running into another issue. This works without minification, so it should be 'fixable'. The error I get is this:

Property 'ViewData' is of type 'Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary1[[Website.App.ServiceModel.RhymeResponse, VandaleRijm.Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]', but this method requires a value of type 'Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary1[[System.Object, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]'. Parameter name: viewContext

It seems that the ViewDataDictionary is of the wrong type... This happens when the BaseViewPage is different from the default. When I change this:

@inherits Website.App.Framework.BaseViewPage<Website.App.ServiceModel.RhymeResponse>

to this:

@model Website.App.ServiceModel.RhymeResponse

This does not throw an error. Any way to set the base page class?

@RobSchoenaker
Copy link
Author

Constructing the html minifier gives another error on pages. Seems the quotes need to stay:

private HtmlMinifier _htmlMinifier = new HtmlMinifier(new HtmlMinificationSettings() {
  RemoveOptionalEndTags = false,
  AttributeQuotesRemovalMode = HtmlAttributeQuotesRemovalMode.KeepQuotes,
});

@RobSchoenaker
Copy link
Author

RobSchoenaker commented Sep 29, 2017

And as it appears, there is some more to be done... I needed to modify quite a lot of code to get it working and still there is room for improvement. Quite obvious, since html is not the same as a razor page.
Minifying html may not be the best way to minify the Razor pages, so this may need additional tweaking. Is there any way I can share my changes? Posting the complete code here is too much.
I added an option that when minifying fails, I perform some custom minifying by removing unwanted whitespace. This removes 90% of the whitespaces from the Razor views, which makes compilation faster too.

@RobSchoenaker
Copy link
Author

OK, managed to get is working completely. Needed to change the build behaviour for publishing too. Want to see the result? Here is the site:
https://rijmwoordenboek.vandale.nl/rijm

@guardrex
Copy link
Owner

guardrex commented Oct 1, 2017

I made a few tweaks here, too.

  • _ViewStart.cshtml and _ViewImports.cshtml can be skipped entirely.
  • It's probably better to split the directives from the markup using ;\r\n< ... the last semicolon (of a Razor directive line) with a line break (\r\n) followed by a less than (<) that should be an HTML tag of some sort. As you pointed out if this isn't found, the text to minify can start at the beginning of the file.

... so I have this now ...

if (projectItem.FileName != "_ViewStart.cshtml" && projectItem.FileName != "_ViewImports.cshtml")
{
    var markup = string.Empty;
    var directives = string.Empty;
    var markupStart = text.IndexOf(";\r\n<");
    if (markupStart != -1)
    {
        directives = text.Substring(0, markupStart + 3);
        markup = text.Substring(markupStart + 3);
    }
    else
    {
        markup = text;
    }
    text = directives + Minify(markup);
}

On the quoted attribute values, my test page here works without quoted attribute values. I put a note in for now, so the top of the code looks like this ...

// If pages fail due to missing attribute quotes, add:
//
// AttributeQuotesRemovalMode = HtmlAttributeQuotesRemovalMode.KeepQuotes,
//
// to the HtmlMinificationSettings.
private HtmlMinifier _htmlMinifier = new HtmlMinifier(new HtmlMinificationSettings() {
    RemoveOptionalEndTags = false,
});

@guardrex
Copy link
Owner

guardrex commented Oct 1, 2017

RE:

@inherits Website.App.Framework.BaseViewPage<Website.App.ServiceModel.RhymeResponse>
to this:
@model Website.App.ServiceModel.RhymeResponse

... How did you solve the problem with the base class?

RE:

Minifying html may not be the best way to minify the Razor pages

There's another approach that involves minifying content in the C# code page file. That approach would take the generated code that the Razor engine produces and minify the content of each WriteLiteral line. For example, here's a line that writes out some HTML ...

WriteLiteral(\"\\r\\n    <p>Run a query:</p>\\r\\n    <p>\\r\\n        \");

I think that if all of the content in all of these lines is minified that the output would be fully minfied. Since minifying in the WriteLiterals content doesn't touch anything else the Razor engine is doing, it should be vastly simpler to minify this way. There's one small catch tho! ... The generated file is a string. I think a RegEx should work to isolate all of the WriteLiteral lines, then they can have their content minified. no ... no ... actually, it's the RazorCSharpDocument that they provide access to; therefore, it might be really easy (if that document goes by lines) to work with each WriteLiteral line. I'll see right now if I can make it work and report back to you.

@RobSchoenaker
Copy link
Author

CustomRazorTemplateEngine.zip
That is the same approach I took. This is the class I have here and this works like a charm:

@guardrex
Copy link
Owner

guardrex commented Oct 1, 2017

ah ... good ... yeah ... that's going to work a lot better.

@RobSchoenaker
Copy link
Author

RobSchoenaker commented Oct 1, 2017

I did not resolve the base class yet. Suppose that is arranged from the 'RazorEngingHost', another clas, which I did not figure out yet. Not really a showstopper, since I managed to get it working without the specific base class.

The trick was basically to eliminate all \n instances (replaced them with \r) and then replace all \r\r with a single \r. This magically minifies the complete Razor page - but it does not minify any JS or CSS. (just made sure to trim all lines before adding them to the output)

It would probably a good idea to have the JS and CSS bits in a separate non-Razor partial (like the first sample you had) and have that minified by WebMarkupMin. The Razor pages can then be minified by the \r\r replacer. (This is what it actually does at the moment)

@guardrex
Copy link
Owner

guardrex commented Oct 1, 2017

I see.

The code you sent ... it's still not quite what I have in mind. I'm not sure it will be possible, but I'd like to run through the RazorCodeDocument or the RazorCSharpDocument and see if I can get access to the WriteLiteral lines, then I'll just minify the string content of those lines. That should do it ... and it would be vastly simpler than anything else.

@RobSchoenaker
Copy link
Author

Overriding the WriteLiteral would mean modifying the code-generator. It would be beneficial, but requires a lot more effort... I opted to not go down that road, since the WriteLiteral is auto generated. Controlling what is sent to the compiler fixes this too (see my code)
Alo, keep in mind that every new Partial would add a linebreak to the ouput, which is now fixed by changing everything to \r instead of \r\n or \r\r

@RobSchoenaker
Copy link
Author

Any news? Anxiuosly waiting to see what you changed and how it works :)

@guardrex
Copy link
Owner

guardrex commented Oct 3, 2017

Some news ... I did update my blog with some of the changes we discussed. I totally agree with you on messing with the code generator. It's a GIANT PITA to do that. I started to build that solution out, but I didn't finish it last weekend. It's what @Anderman did for the first version of Razor minification using the Razor engine in 1.0: https://github.com/guardrex/GuardRex.MinifyingMvcRazorHost. He did it in a couple of hours b/c he knows what he's doing. I might be able to pick back up with it. I'm about to start a new MS contract, so I'm about to get slammed. I'm not sure I'll get back to it, but I hope I'll have time this weekend to work on it a bit more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants