6

I'm trying to parse and compile Razor templates in a sandboxed environment a.k.a. a custom host based on this information (architecture see below).

I'm having troubles getting intellisense to work, so i specified a BuildProvider as stated here and followed the 'workaround' provided in the answer to that question.

On @model MyAssembly.MyModel intellisense gives the following error:

Could not load file or assembly 'System.Web.WebPages.Razor' or one of it's dependencies.

(the assembly is referenced and copied local, as well as all other related Razor assemblies)

When parsing and compiling a template however the following error is thrown:

Line: 33 Col: 7 Error: The name 'model' does not exist in the current context

Any leads/suggestions?

p.s. If i remove the @model directive the template parses and compiles fine

Architecture:

  • Webapplication: references Class library and provides the .cshtml template files using a model from the 3d party class library.
  • Class library: contains RazorHost and BaseTemplate and references 3d party library to add model to .cshtml files provided by webapplication.
  • 3d Party Class: provides model for webapplication
Community
  • 1
  • 1
Ropstah
  • 17,538
  • 24
  • 120
  • 194

4 Answers4

6

@model is something very specific to MVC's implementation of Razor. As such, out of the box, it doesn't work. I've uploaded a patch to the RazorEngine on codeplex that adds @model support to it's engine and it would be pretty easy to implement it outside of that specific version. http://razorengine.codeplex.com/SourceControl/list/patches

It basically involves overriding the CodeGenerator that razor uses to generate it's class files and override TryVisitSpecialSpan

protected override bool TryVisitSpecialSpan(Span span) {
    return TryVisit<ModelSpan>(span, VisitModelSpan); 
      //This is where you would add more special span tests 
      //|| TryVisit<SomeOtherSpan>(span, Method);
}

void VisitModelSpan(ModelSpan span) {
    string modelName = span.ModelTypeName;

    if (DesignTimeMode) {
        WriteHelperVariable(span.Content, "__modelHelper");
    }
}

Then you also have to create your own CSharpCodeParser

    public class CSharpRazorCodeParser : CSharpCodeParser {
        public string TypeName { get; set; }

        public CSharpRazorCodeParser() {
            RazorKeywords.Add("model", WrapSimpleBlockParser(System.Web.Razor.Parser.SyntaxTree.BlockType.Directive, ParseModelStatement));
        }

        bool ParseModelStatement(CodeBlockInfo block) {
            End(MetaCodeSpan.Create);

            SourceLocation endModelLocation = CurrentLocation;

            Context.AcceptWhiteSpace(includeNewLines: false);

            if (ParserHelpers.IsIdentifierStart(CurrentCharacter)) {
                using (Context.StartTemporaryBuffer()) {
                    AcceptTypeName();
                    Context.AcceptTemporaryBuffer();
                }
            } else {
                OnError(endModelLocation, "Model Keyword Must Be Followed By Type Name");
            }

            End(ModelSpan.Create(Context, TypeName));

            return false;
        }
    }

And even after that you have to override the Host to use your new classes

public class RazorEngineHost : System.Web.Razor.RazorEngineHost {

    public RazorEngineHost(RazorCodeLanguage codeLanguage, Func<MarkupParser> markupParserFactory)
        : base(codeLanguage, markupParserFactory) { }

    public override System.Web.Razor.Generator.RazorCodeGenerator DecorateCodeGenerator(System.Web.Razor.Generator.RazorCodeGenerator generator) {
        if (generator is CSharpRazorCodeGenerator) {
            return new CSharpRazorCodeGenerator(generator.ClassName,
                                                   generator.RootNamespaceName,
                                                   generator.SourceFileName,
                                                   generator.Host, false);
        }

        return base.DecorateCodeGenerator(generator);
    }

    public override ParserBase DecorateCodeParser(ParserBase incomingCodeParser) {
        if (incomingCodeParser is CSharpCodeParser) {
            return new CSharpRazorCodeParser();
        } else {
            return base.DecorateCodeParser(incomingCodeParser);
        }
    }
}

You also have to create your own custom CodeSpan

public class ModelSpan : CodeSpan {
    public ModelSpan(SourceLocation start, string content, string modelTypeName) : base(start, content) {
        this.ModelTypeName = modelTypeName;
    }

    public string ModelTypeName { get; private set; }

    public override int GetHashCode() {
        return base.GetHashCode() ^ (ModelTypeName ?? String.Empty).GetHashCode();
    }

    public override bool Equals(object obj) {
        ModelSpan span = obj as ModelSpan;
        return span != null && Equals(span);
    }

    private bool Equals(ModelSpan span) {
        return base.Equals(span) && string.Equals(ModelTypeName, span.ModelTypeName, StringComparison.Ordinal);
    }

    public new static ModelSpan Create(ParserContext context, string modelTypeName) {
        return new ModelSpan(context.CurrentSpanStart, context.ContentBuffer.ToString(), modelTypeName);
    }
}

This implementation doesn't do anything other than tell the designer what model to use. It shouldn't affect compilation at all but allow the compiler to ignore this particular command.

Buildstarted
  • 26,529
  • 10
  • 84
  • 95
  • This works indeed for design time, so thanks for that. However at runtime the same error is thrown: `Line: 33 Col: 7 Error: The name 'model' does not exist in the current context`. Now that I think of it, the model isn't passed to the view anywhere... How should this be done? – Ropstah Apr 29 '11 at 12:31
  • 1
    Make sure you're overriding everything you need to override in your code to make this code the one that's called instead of the default razor host and what not. You could download the RazorEngine code at http://razor-engine.com/ and check out how that one works to compare with your code. Without seeing your implementation it will be hard to guess where the error lies. – Buildstarted Apr 29 '11 at 14:04
  • Cool, I'm currently working on a blog post to get this out there - though I've been lazy :) – Buildstarted May 04 '11 at 14:30
  • It would be great if you could finish that blog post... I have used the code, works well, except that you forgot to set TypeName in the code parser. It would be nice to know why the return value of ParseModelStatement is always false, its' badly documented by microsoft. Absolutely excellent code sample otherwise and works as advertised. – Jim Nov 10 '11 at 04:55
  • Is it possible to change the DefaultBaseType of the template inside the derived host? I am parsing my template twice to get the model, and then again once the template base type has been extracted - which is clearly very inefficient. – Jim Nov 10 '11 at 04:57
  • please visit https://github.com/Antaris/RazorEngine for the latest version of the source which is a complete rewrite and has support for @model. – Buildstarted Nov 10 '11 at 05:02
1

There is a simple solution for IntelliSense to work with 'custom Razor environment' and Resharper (e.g. to use RazorEngine for reporting). To Enable IntelliSense create the following class in your project:

public class EnableIntelliSenseFor<T> 
{
  public readonly T Model;
}

At the top of your .cshtml file add this line:

@inherits EnableIntelliSenseFor<YourModelType>

Then when parsing the template simply remove the top line as suggested by ThiagoPXP with this:

template = RemoveInheritsDirective(template);            
var html = Razor.Parse(template, model);     

private static string RemoveInheritsDirective(string template)
{
   return template.StartsWith("@inherits") 
      ? template.Substring(template.IndexOf('\n') + 1) 
      : template;
}

Access your model with @Model and IntelliSense should work as expected.

Gregor Slavec
  • 4,814
  • 1
  • 26
  • 24
0

I had the same issue and my solution was quite simple.

having the @model at the beginning of the file is great cause it gives us some intellisense. However, it breaks the razor engine so my solution was to remove the model declaration on runtime before call the parser.

string template = File.ReadAllText(@"C\myRazorView.cshtml");

var model = new MyViewModel { Name = "Foo", Surname = "Bar" };

//remove model declaration from the view file
template = template.Replace("@model MyViewModel", "");
string result = Razor.Parse(template, model);

In my case all views are using the same model, therefore using String.Replace() in the first line did the trick for me.

You could enhance it removing the first line in another way like regex or something else.

ThiagoPXP
  • 5,362
  • 3
  • 31
  • 44
0

For me this worked (using RazorEngine 3.3 on NET4.0):

1.this line to top of cshtml

@inherits RazorEngine.Templating.TemplateBase<MyModel>

2.this in Page_Load

var templateName = System.IO.Path.ChangeExtension( Request.PhysicalPath, "cshtml");
var template = System.IO.File.ReadAllText(templateName);
var r = Razor.Parse<MyModel>(template, new MyModel {
                FileName = "Example.pdf",
                MessageId = Guid.NewGuid()
            }, "MyPage");
Response.Write(r);
Jiří Zídek
  • 416
  • 3
  • 9