wdcossey / RazorEngineCore.Extensions

Extensions for RazorEngineCore (ASP.NET Core 3.1.1 Razor Template Engine)
11 stars 5 forks source link

Patch to implement Html.PartialAsync #9

Open kobruleht opened 3 years ago

kobruleht commented 3 years ago

Using @Html.PartialAsync in template causes error.

Add partial support using code like

  protected async Task<IHtmlstring> PartialAsync<T>(string viewName, T model)
        {
            // http://stackoverflow.com/questions/483091/render-a-view-as-a-string
            ViewData.Model = model;
            using (var sw = new StringWriter())
            {
                var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName);
                var viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
                viewResult.View.Render(viewContext, sw);
                viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View);
                return new Htmlstring  (sw.GetStringBuilder().ToString());
            }
        }
adoconnection commented 3 years ago

@kobruleht what kind of app you are building right now? (to understand your needs better) the code you provided has is more about MVC rather the RazorEngine itself.

kobruleht commented 3 years ago

I'm building ASP.NET 5 MVC Shopping cart + ERP application.

RazorEngineCore.Extension is used to inject Razor code from database into application razor views:


        public async Task<string> RenderAsync<TModel>(string template, TModel model, ITempDataDictionary tempData) 
        {
            int templateHashCode = template.GetHashCode();
            if (!templateCache.TryGetValue(templateHashCode, out object vaart))
            {
                vaart = await CompileTempate<TModel>(template);
                templateCache.TryAdd(templateHashCode, vaart);
            }

            var compiledTemplate = (IRazorEngineCompiledTemplate<MyRazorEngineCorePageModel<TModel>>) vaart;
                return await compiledTemplate.RunAsync(instance =>
                {
                    instance.Model = model;
                    instance.TempData = tempData;
                    instance.HttpContext = httpContextAccessor.HttpContext;
                });
        }

        async Task<IRazorEngineCompiledTemplate<MyRazorEngineCorePageModel<TModel2>>> CompileTempate<TModel2>(string template)
        {
            var razorEngine = new RazorEngine();
                var compiledTemplateLocal = await razorEngine.CompileAsync<MyRazorEngineCorePageModel<TModel2>>(template, builder =>
                {
                    builder.AddAssemblyReference(typeof(RazorEngineCorePageModel));
                    builder.AddAssemblyReference(typeof(ITempDataDictionary));
                    builder.AddAssemblyReference(typeof(MyScaffoldContext));
                    builder.AddAssemblyReference(typeof(DbContext));
                });
                return compiledTemplateLocal;
        }

    public abstract class MyRazorEngineCorePageModel<T> : RazorEngineCorePageModel, IRazorEngineTemplate
    {
        public ITempDataDictionary TempData;
        public HttpContext HttpContext;
    }
adoconnection commented 3 years ago

Why dont you use Razor that comes together with MVC?

kobruleht commented 3 years ago

MVC does not allow to use templates from database. Those templates are created buy application users and needs rendered runtime, for example to create new shipping methods in shopping cart.

wdcossey commented 3 years ago

As per my comment here, I'll have a look and see what I can do 😃

kobruleht commented 3 years ago

Thank you. It should be async version, @Html.PartialAsync since sync version causes locks and will removed from .NET 6

wdcossey commented 3 years ago

I would have no issues adding asynchronous support but there will need to be changes added to the base project, template writing currently only supports synchronous writing (see here).

I will make the changes and raise a PR.

kobruleht commented 3 years ago

I posted it in https://github.com/adoconnection/RazorEngineCore/issues/48

wdcossey commented 3 years ago

@kobruleht

I have implemented the following:

@Html.Partial("Partial", new { Name = "Partial Template"})

@(await Html.PartialAsync("Partial", new { Name = "Partial Template"}))

/* Note that the second parameter (model) is optional and will pass through the parent model if not specified */
@(await Html.PartialAsync("Partial"))

I originally had plans for Html.PartialAsync(Func<string> func, ...), unfortunately Func<> is not supported :/

I have added a basic PartialViewsManager (it's the name I came up with at the time) to manage the Partial views used by the extensions. PartialViewsManager is a static class that implements a ConcurrentDictionary<string, Lazy<...>>, you can define your partials views before using .RunAsync(...)

In a nutshell, you can so the following.

// setup
PartialViewsManager.TryAdd("Body", SampleApp.Content.BodyContent);
PartialViewsManager.TryAdd("Partial", SampleApp.Content.PartialContent);

Primary template:

<!DOCTYPE html>
<html lang="en">
<body>
    @(await Html.PartialAsync("Body"), new { Name = "Body"})
</body>
</html>

Body template:

<div>
    <main>
        Hello <b>@Model.Name</b>
        @(await Html.PartialAsync("Partial", new { Name = "Partial Template"}))
    </main>
</div>

Additional template:

Hello <b>@Model.Name</b>

This will render:

<!DOCTYPE html>
<html lang="en">
<body>
<div>
    <main>
        Hello <b>Body</b>
        Hello <b>Partial Template</b>
    </main>
</div>
</body>
</html>
kobruleht commented 3 years ago

Will it allow to use existing, pre-compiled views in applicaton, without using PartialViewsManager ?

@await Html.ParialAsync("_PartialName")

should find view in Views/Shared directory without additional settings required. In .NET 4 Antalis RazoreEngine does not have view manager. Not sure is view manager required.

wdcossey commented 3 years ago

@kobruleht

I am still looking into this, I will get to it later this evening (UK time here). I did forget about pre-compiled partials/views but will ensure they work as expected.

It's worth noting that RazorEngineCore is NOT AspNetCore so things do behave differently (as I am sure you are aware), there's no mechanism to find templates as you would have in AspNetCore. It's possible to implement some rudimentary finder/lookup mechanism if you are going to use .cshtml templates from a Content resource (rather than strings) which would be better IMHO.

Making changes to the main repo owned by adoconnection would be simpler but I think he's against the whole Partial views thing (in his project), I have to make workarounds here to compensate for what I think is lacking (hence why this project exists). I am limited in options when it comes to extending RazorEngineCore w/o forking the entire project and creating another package (which there's a bunch [of broken ones] already on NuGet).

That all said, I will happily get something working and you are most welcome to add input, make requests and/or raise PRs for anything you need.

kobruleht commented 3 years ago

Code and SO link which I posted in this issue finds pre-compiled views by name and renders it as string:

var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName);
var viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw);
 viewResult.View.Render(viewContext, sw);
viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View);
return new Htmlstring  (sw.GetStringBuilder().ToString());

HtmlPartialAsync should run this code and return result string.

If application is published, there are no .cshtml files. Only compiled assemblies are published. Builtin views contain @inject commands which are not supported in RazorEngineExtensions. So those cannot compiled. Since compiled code can used there is no reason to add cshtml files as resources.

Maybe it is possible to leave RazorEngineCore without MVC and dependency injection like author prefers and implement MVC specific thinga like Html.PartialAsync and @inject in your project using template base class. In this case using MVC application willl require two packages included.

wdcossey commented 3 years ago

I am implementing the following.

RazorEngineCoreCompiler.MSBuild.Tasks (this replaces the old RazorEngineCore.Precompiler which never really worked)

To use the embedded templates should be as simple as.

var resourceTemplate = await PrecompiledTemplate.LoadAsync("Template", Assembly.GetExecutingAssembly());
await resourceTemplate.RunAsync(model: model);

These precompiled templates can have @Html.PartialAsync() (or @Html.Partial()), scanning for precompiled templates will be against the EntryAssembly (for now).

There's a bunch of things that still need to be optimised but here's a basic benchmark (Runtime vs Precompiled) of rendering templates

Results of 10 runs |
        Runtime    | total: 00:00:01.0446029; average: 00:00:00.1044602
        Precompile | total: 00:00:00.2078749; average: 00:00:00.0207874
kobruleht commented 3 years ago

No sure is this required:

Scripts are created at runtime by users or by code using StringBuilder. Desing-time scripts can added into Views as .cshtml files. At build time they are compiled automatically. At runtime compiled code can retrieved by view name using

ViewEngines.Engines.FindPartialView(ControllerContext, viewName)

wdcossey commented 3 years ago

@kobruleht

Can you please tell me what you are attempting to achieve? can you create a repo and share the code (even if it's real razor pages or mvc) so I can understand.

You keep posting the same code over again and I am not sure why.

ViewEngines.Engines.FindPartialView(ControllerContext, viewName)

RazorEngineCore has no concept of ControllerContext or ViewControllers

In RazorPages (AspNetCore) the Pages (a.k.a Views) are compiled during build to a satellite assembly (something like <assembly>.Views.dll), (from my understanding) this is typically where AspNetCore will go find them. Calling @Html.PartialAsync() will go find this precompiled razor code during runtime, partial views are not rendered into the main page during build (the partial [raw cshtml] isn't injected into the main page during the build).

From my understanding you do NOT want precompiled templates in your implementation as you said "templates are created buy application users and needs rendered runtime", "inject Razor code from database into application razor views" Am I misunderstanding this? Do you use raw (string) templates in your database?

I think if you could create a small repo and show what your intensions are this would be a lot easier to understand.

There are many ways to combat the issue(s) w/o the need for adding additional dependencies.

kobruleht commented 3 years ago

There is partial view _LoginOrRegister in project in

Views\Shared\_LoginOrRegister.cshtml

Can your package allow to use it using

            RazorEngine razorEngine = new RazorEngine();
            var template =
                await razorEngine.CompileAsync<RazorEngineCorePageModel>(
                    @"
@await HtmlPartialAsync(""_LoginOrRegister"")
");
            var res = await template.RunAsync();

?

wdcossey commented 3 years ago

Absolutely, that's what I am working on at the moment.

There will be options to have the .cshtml either ...

Other things worth mentioning...

I am currently testing the following:

@(await Html.PartialAsync("PartialTemplate", new { Name = "Partial Template"}))

With this as my PartialTemplate.cshtml

@inherits RazorEngineCore.RazorEngineCorePageModel

Hello <b>@Model.Name</b>

If you are interested, I can push a development branch that you can pull and run some of your own tests against the SampleApp included?

kobruleht commented 3 years ago

Thank you. I can try to use it in my application calling _LoginOrRegister and similar partial views. I want to allow users to create their own pages as razor views by using predefined partial views for logon, register, account and similar tasks.

wdcossey commented 3 years ago

@kobruleht

I am almost done with the package, hopefully I will get it out in the next few days (very busy at the moment), apologies for the wait.

kobruleht commented 3 years ago

Thank you. Great news. How are things going?

wdcossey commented 3 years ago

I will get around to uploading a pre-release to Nuget this evening. I'll also post the details of what's been done and what you can and can't do with the update. :)

wdcossey commented 3 years ago

@kobruleht

Apologies for the delay, I have been ill for a few days.

The package is uploading to NuGet, version 0.5.0-alpha.1 (please check that you include prerelease versions in your NuGet search).

You can use partial views as you would expect in Asp.NetCore Razor pages

i.e

@(await Html.PartialAsync("PartialName", new { Name = "Something"}))
//OR
@(await Html.PartialAsync("PartialName")) // without specifying the Model (it will be inherited)

Names of the Partial Views are created from the name of the .cshtml file.

i.e Pages\Template.cshtml becomes Template

@(await Html.PartialAsync("Template"))

Pages\Shared\_SharedTemplate.cshtml becomes Shared/_SharedTemplate

@(await Html.PartialAsync("Shared/_SharedTemplate")
//OR
@(await Html.PartialAsync("_SharedTemplate") //if there's only a single `_SharedTemplate`

Sample Usage:

var resourceTemplate = await PrecompiledTemplate.LoadAsync("Template", Assembly.GetExecutingAssembly());

Behaviours:

The default behaviour is to precompile and embed the templates in the output assembly, if this is undesirable you can disable this

    <PropertyGroup>
        <RazorEngineCoreExtEmbedResources>False</RazorEngineCoreExtEmbedResources>
        <RazorEngineCoreExtPrecompile>False</RazorEngineCoreExtPrecompile>
    </PropertyGroup>

RazorEngineCoreExtEmbedResources is used to embed the files, set this to false to copy the files to the output. RazorEngineCoreExtPrecompile is used for precompiling the .cshtml files (into .rzhtml), set this to false to disable this feature.


Additional Configuration:

The default location for scanning for .cshtml files is Pages (ala Razor pages), if this is undesirable, you can change the following

    <PropertyGroup>
        <RazorEngineCoreExtViewsDirectory>Views</RazorEngineCoreExtViewsDirectory>
    </PropertyGroup>

Notes:

You mentioned allow users to create their own pages as razor views by using predefined partial views for logon, register, account and similar tasks.

I have added logic to the Partial View lookup, if you precompile and/or embed templates into the output assembly and the file exists on the file system (at the original [relative] location), this file will be used rather than the precompiled or embedded ones (thus allowing you to override them).

Scenario 1:

Original Template file = Pages\Login.cshtml RazorEngineCoreExtPrecompile = True RazorEngineCoreExtEmbedResources = True This would embed the (pre)compiled template into the output assembly (as expected) Creating (Adding) a file Pages\Login.cshtml would override the (pre)compiled and embedded template.

Scenario 2:

Original Template file = Pages\Login.cshtml RazorEngineCoreExtPrecompile = True RazorEngineCoreExtEmbedResources = False This would copy (pre)compiled Pages\Login.rzhtml to the output. Creating (Adding) a file Pages\Login.cshtml would override this (pre)compiled template.


Final Words:

There's still some things that need to be ironed out and optimized but hopefully it works as intended.

kobruleht commented 3 years ago

It causes build error

Error   MSB4018 The "RazorEngineCoreCompiler" task failed unexpectedly.
System.IO.FileNotFoundException: Could not load file or assembly 'System.Runtime, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.
File name: 'System.Runtime, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
   at RazorEngineCoreCompiler.MSBuild.RazorEngineCoreCompiler.Execute()
   at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute()
   at Microsoft.Build.BackEnd.TaskBuilder.<ExecuteInstantiatedTask>d__26.MoveNext()

WRN: Assembly binding logging is turned OFF.
To enable assembly bind failure logging, set the registry value [HKLM\Software\Microsoft\Fusion!EnableLog] (DWORD) to 1.
Note: There is some performance penalty associated with assembly bind failure logging.
To turn this feature off, remove the registry value [HKLM\Software\Microsoft\Fusion!EnableLog].
    Eeva    C:\Users\andrus\.nuget\packages\razorenginecore.extensions\0.5.0-alpha.1\build\net5.0\RazorEngineCore.Extensions.targets    31  

Clean solution and rebuild all does not fix it. So I reverted back to previous version.

wdcossey commented 3 years ago

Hi @kobruleht,

There's a chance that I missed something, hence why there's a preview build.

Could you create a repo on GitHub and upload a sample project?

Also, what versions of dotnet do you have installed on your system?

'dotnet --list-sdks'

'dotnet --list-runtimes'

kobruleht commented 3 years ago

Partial view content from script is empty. I created testcase in https://github.com/kobruleht/RazorEngineCoreExtensionsTest

wdcossey commented 3 years ago

@kobruleht

Thanks for the sample code, it's a great help!

I can see a few issues and will get them fixed as soon as possible.

kobruleht commented 3 years ago

Thank you. Great. How are things going.

wdcossey commented 3 years ago

Hi @kobruleht

I need some changes pushed upstream to make things a little simpler, I will push a PR this week. :)

kobruleht commented 3 years ago

Thank you. I also tried

@model myModel but got compile error

RazorEngineCompilationException: Unable to compile template: cjgmehoi.y0p(7,7): error CS0103: The name 'model' does not exist in the current context

It generates code

` Write(model);

WriteLiteral(" myModel\r\n\r\n<!DOCTYPE html>\r\n\r\n\r\n <meta charset=\"utf-8\" />\r\n "); ` @model is required for intellisense in Visual Studio Razor Editor. @inherits directive which is also required for intellisense is ignored as expected.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/kobruleht"><img src="https://avatars.githubusercontent.com/u/1274923?v=4" />kobruleht</a> commented <strong> 3 years ago</strong> </div> <div class="markdown-body"> <p>@wdcossey Any news ?</p> </div> </div> <div class="page-bar-simple"> </div> <div class="footer"> <ul class="body"> <li>© <script> document.write(new Date().getFullYear()) </script> Githubissues.</li> <li>Githubissues is a development platform for aggregating issues.</li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <script src="/githubissues/assets/js.js"></script> <script src="/githubissues/assets/markdown.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/highlight.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/languages/go.min.js"></script> <script> hljs.highlightAll(); </script> </body> </html>