I want to use the ViewPage/HtmlHelper class in the System.Web.Mvc namespace in a seperate project. I imported the relevant libraries and then tried this:
using System.Web.Mvc;
using System.Web.Mvc.Resources;
using System.Web.Mvc.Html;
public static class Display
{
public static string CheckBox()
{
ViewPage viewPage = new ViewPage();
return viewPage.Html.CheckBox("Test");
}
}
Which I call like this in another class that includes my display class:
string Checkbox = Display.CheckBox():
This compiles just fine, however when I run it I get:
System.NullReferenceException: Object
reference not set to an instance of an
object.
I simply want to use the HtmlHelper's extension methods as is, e.g: page.Html.ActionLink(), page.Html.Radionbutton() etc. How can I resolve this problem?
Are you trying to call your custom CheckBox() from a different place than the view? Please don't do that. The philosophy behind ASP.NET MVC is that your controller should prepare all data for the view, then the view should decide on how to render it.
If you redesign your method to be an extension method, you could do that:
public static class Display // class name really don't matter for extension methods
{
public static string CheckBox(this HtmlHelper html)
{
return html.CheckBox("Test");
}
}
Within the view:
<%= Html.CheckBox() %>
Note that this may cause a naming conflict with existing extension methods. One way to avoid that is to design something like:
New code in view:
<%= Html.Display().CheckBox() %>
New extension code:
public static DisplayExtension
{
public static Display(this HtmlHelper html)
{
return new Display(html);
}
}
public class Display // no longer static
{
private readonly HtmlHelper html;
public string Display(HtmlHelper html)
{
this.html = html;
}
public string CheckBox()
{
return html.CheckBox("Test");
}
}
The Html helpers require that the ViewContext property of the ViewPage is set. Typically, this is not the case within a controller or other class code.
Could you package this CheckBox within an ASCX file and reference it there by other views with a Html.RenderPartial method call?
Related
I am quite novice in ASP.NET Core.
I am trying to migrate our old project to ASP.NET core.
We use a lot of script blocks inside of partials that are centrally rendered in predefined place of layout. We use HtmlHelper with disposable pattern as described here. The problem is that due of the new architecture in ASP.NET core it is not possible to use
webPageBase.OutputStack.Pop()
to catch content. I found similar solution with TagHelpers
but I still will prefer to stay with HtmlHelper extension to have collecting scripts logic and rendering in single place. Otherwise I will need to write 2 tag helpers, coordinate them and replace all 50-100 occurrences of HtmlHelper extension to tag helper.
HTML helpers are still a thing in ASP.NET Core. Just because tag helpers are the new and generally more flexible solution to render custom HTML, that does not mean that HTML helpers are gone or that they have no use left. The built-in tag helpers are actually based on the HTML helpers and will use the same internal backend to generate the output. So it’s just a different interface for the same thing.
That being said, due to how ASP.NET Core renders views, capturing the content inside a using block is a bit more difficult compared to how it works in tag helpers (where it’s a very general feature).
I’ve been sitting on this for a while now and came up with the following. This works by temporarily replacing the view writer to a StringWriter for as long as the block is open. Note that this might be a super terrible idea. But it works…
public static class ScriptHtmlHelper
{
private const string ScriptsKey = "__ScriptHtmlHelper_Scripts";
public static ScriptBlock BeginScripts(this IHtmlHelper helper)
{
return new ScriptBlock(helper.ViewContext);
}
public static IHtmlContent PageScripts(this IHtmlHelper helper)
{
if (helper.ViewContext.HttpContext.Items.TryGetValue(ScriptsKey, out var scriptsData) && scriptsData is List<object> scripts)
return new HtmlContentBuilder(scripts);
return HtmlString.Empty;
}
public class ScriptBlock : IDisposable
{
private ViewContext _viewContext;
private TextWriter _originalWriter;
private StringWriter _scriptWriter;
private bool _disposed;
public ScriptBlock(ViewContext viewContext)
{
_viewContext = viewContext;
_originalWriter = viewContext.Writer;
// replace writer
viewContext.Writer = _scriptWriter = new StringWriter();
}
public void Dispose()
{
if (_disposed)
return;
try
{
List<object> scripts = null;
if (_viewContext.HttpContext.Items.TryGetValue(ScriptsKey, out var scriptsData))
scripts = scriptsData as List<object>;
if (scripts == null)
_viewContext.HttpContext.Items[ScriptsKey] = scripts = new List<object>();
scripts.Add(new HtmlString(_scriptWriter.ToString()));
}
finally
{
// restore the original writer
_viewContext.Writer = _originalWriter;
_disposed = true;
}
}
}
}
Usage will be like this:
#using (Html.BeginScripts()) {
<script>console.log('foo');</script>
<script>console.log('bar');</script>
}
#using (Html.BeginScripts()) {
<script>console.log('baz');</script>
}
And then to render everything:
#Html.PageScripts()
I know there is a way to ad c# functions inside a view and call them by using #functions{ ... } method inside my view, but is there a way to create a shared view with those functions to include inside of the controllers view without copying the same line of code on each one? I tried by using #inject and other methods inside the _Layout view, but obviously those methods can't be called. I also tried to create an external class like this, but I want to use views only if it is possible:
public class Functions : RazorPage<dynamic>
{
public override Task ExecuteAsync()
{
throw new NotImplementedException();
}
public string GetTabActive(string lang)
{
if (ViewBag.lang.ToString() == lang) return "active";
return "";
}
}
I finally found a way to do this, i needed to inject the class inside of the _ViewImports giving a property name, like so:
#inject Functions func
And, in the StartUp, i added a new service pointing to my abstract class like that:
services.AddSingleton<Functions>();
So, inside each view, i can use models and call my functions like that:
<h2>#func.MyFunction</h2>
Create an abstract class that inherits WebViewPage
public abstract class TestView<TViewModel> : WebViewPage<TViewModel>
{
public void TestMethod()
{
}
}
In your views use "Inherits"
#inherits TestView<dynamic>
Your method will be available
#TestMethod()
--Side note
You should not use #model in conjunction with #inherits
You just want one or the other.
There are a few approaches:
Approach 1: Subclass RazorPage<TModel>
You can (ab)use OOP inheritance to add common members (i.e. methods/functions, but also properties) to your Razor page types by subclassing RazorPage<T> and updating all of your pages to use your new subclass as their base type instead of defaulting to ASP.NET Core's RazorPage or RazorPage<TModel>.
In your project, subclass Microsoft.AspNetCore.Mvc.Razor.RazorPage or Microsoft.AspNetCore.Mvc.Razor.RazorPage<TModel>, e.g. class MyPage<TModel> : RazorPage<TModel>
Add whatever methods you like (protected or public, and static or instance all work).
In all .cshtml files where you want to use them, change your #model MyPageModel directive to #inherits MyPage<MyPageModel>.
Note that:
The class RazorPage<TModel> derives from the (non-generic) class RazorPage class, so subclassing RazorPage<TModel> will not cause those members to be visible from pages deriving from RazorPage.
Note that if your .cshtml lacks a #model directive, the actual page class will derive from RazorPage<dynamic> instead of RazorPage.
You can, of course, subclass RazorPage<TModel> with a non-generic class provided you specify a concrete type for TModel in your subclass; this is useful when you have multiple .cshtml pages that share the same #model type and need lots of custom C# logic.
For example:
MyPage.cs:
using Microsoft.AspNetCore.Mvc.Razor;
namespace MyProject
{
public abstract class MyPage<TModel> : RazorPage<TModel>
{
protected String Foobar()
{
return "Lorem ipsum";
}
}
}
ActualPage.cshtml:
#using MyProject // <-- Or import in your _ViewImports
#inherits MyPage<ActualPageViewModel>
<div>
#( this.Foobar() ) <!-- Function is inherited -->
</div>
That said, I'm not a super-huge fan of this approach because (in my opinion) subclassing and inheritance should not be abused as a substitute for mixins (though I appreciate that C#'s lack of mixins is a huge ergonomic issue).
Approach 2: Extension methods
You can also define extension methods for RazorPage and RazorPage<TModel>, but also for specific TModel types.
You could also define them for IRazorPage or RazorPageBase if you really wanted to as well.
This is the closest thing we have in C# to generic specialization.
When extending RazorPage<TModel> - and you don't care about TModel, then make it generic type-parameter on your extension-method (see Foobar2 in my example below).
C# extension methods require this. to be used, btw.
And don't forget to import the extension class' namespace (either in the page with #using or in your _ViewImports.cshtml file).
Note th
For example:
MyPageExtensions.cs:
using Microsoft.AspNetCore.Mvc.Razor;
namespace MyProject
{
public static class MyPageExtensions
{
public static String Foobar1( this RazorPage page )
{
return "Lorem ipsum";
}
public static String Foobar2<TModel>( this RazorPage<TModel> page )
{
return "Lorem ipsum";
}
}
}
ActualPage.cshtml:
#using MyProject // <-- Or import in your _ViewImports
#model ActualPageViewModel
<div>
#( this.Foobar() ) <!-- Extension Method -->
</div>
Getting IHtmlHelper, IUrlHelper, etc.
You might notice that RazorPage<TModel> does not have IHtmlHelper Html { get; } nor IUrlHelper Url { get; } properties - nor other useful ASP.NET MVC-specific members. That's because those members are only defined in the hidden PageName.cshtml.g.cs file's class (it's in your obj\$(Configuration)\$(TargetPlatform)\Razor\... directory, if your project builds okay).
In order to get access to those members you can define an interface to expose those properties, and you can direct ASP.NET Core to add that interface to the .cshtml.g.cs classes by adding #implements to your _ViewImports.cshtml file.
This is what I use in my projects:
IRazorPageInjectedProperties.cs:
public interface IRazorPageInjectedProperties
{
ViewContext ViewContext { get; }
Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider ModelMetadataProvider { get; }
Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; }
Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; }
Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; }
Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; }
// Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; }
Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper HtmlHelper { get; }
}
_ViewImports.cshtml:
#using System
#using Microsoft.AspNetCore.Mvc
#* etc *#
#inject Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider ModelMetadataProvider
#inject Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper HtmlHelper
#implements MyProject.IRazorPageInjectedProperties
The IHtmlHelper type needs an explicit #inject directive because interface members have to be public (or explicit interface implementations), but the Html property is protected, but #inject members are public. The name needs to be HtmlHelper instead of Html otherwise it would conflict.
If subclassing RazorPage you might notice your subclass can't really implement IRazorPageInjectedProperties because you'd need to add the properties there (as abstract), but #inject properties won't override them, but you could hack it a bit with some indirect properties, like so:
using Microsoft.AspNetCore.Mvc.Razor;
namespace MyProject
{
public abstract class MyPage<TModel> : RazorPage<TModel>
{
private IRazorPageInjectedProperties Self => (IRazorPageInjectedProperties)this;
private IHtmlHelper Html => this.Self.HtmlHelper;
private IUrlHelper Url => this.Self.Url;
protected IHtmlContent GetAHrefHtml()
{
return this.Html.ActionLink( ... );
}
protected String GetHrefUrl()
{
return this.Url.Action( ... );
}
}
}
If using extension-methods you'll need to either:
Modify IRazorPageInjectedProperties to extend IRazorPage and make IRazorPageInjectedProperties the target of your extension-methods.
See GetAHrefHtml in the example below.
or change your extension methods to be generic over TPage : RazorPage add a type-constraint to require IRazorPageInjectedProperties.
See GetHrefUrl1 in the example below.
to get TModel you'll need to make the extensions generic over TPage and TModel with IRazorPageInjectedProperties.
See GetHrefUrl2 in the example below.
MyPageExtensions.cs:
using Microsoft.AspNetCore.Mvc.Razor;
namespace MyProject
{
public interface IRazorPageInjectedProperties : IRazorPage
{
// etc
}
public static class MyPageExtensions
{
public static IHtmlContent GetAHrefHtml( this IRazorPageInjectedProperties page )
{
return page.Html.ActionLink( ... );
}
public static String GetHrefUrl1<TPage>( this TPage page )
where TPage : RazorPage, IRazorPageInjectedProperties
{
return page.Url.Action( ... );
}
// to get TModel:
public static String GetHrefUrl2<TPage,TModel>( this TPage page )
where TPage : RazorPage<TModel>, IRazorPageInjectedProperties
{
return page.Url.Action( ... );
}
}
}
I want to have a global method like w in my razor view engine for localization my MVC application. I tried
#functions{
public string w(string message)
{
return VCBox.Helpers.Localization.w(message);
}
}
but I should have this in my every razor pages and I don't want that. I want to know how can I have a global function that can be used in every pages of my project?
You can extend the HtmlHelper:
Extensions:
public static class HtmlHelperExtensions
{
public static MvcHtmlString W(this HtmlHelper htmlHelper, string message)
{
return VCBox.Helpers.Localization.w(message);
}
}
Cshtml:
#Html.W("message")
How about an extension method:
namespace System
{
public static class Extensions
{
public static string w(this string message)
{
return VCBox.Helpers.Localization.w(message);
}
}
}
Called like so:
"mymessage".w();
Or:
string mymessage = "mymessage";
mymessage.w();
Or:
Extensions.w("mymessage");
So I have a Session variable which is set during the first user login
Session["ClientID"]
Basically this is used for theming (so the ClientID sets the theme/brand to appear on a website). Looking at the code applying
(Guid)Session["ClientID"]
All over the place just seems dirty and error prone, what the best design pattern to use to get the variable globally recognized. So I can do
this.CurrentClientID
or something similar on all MVC Pages. In theory I could overload the Controller class with a Custom class providing this ID, but I'm not sure how I would expose this to the View as well.
Any pointers to the best solution would be gratefully received!
No idea what you mean globally, but an extension method to the ControllerBase class would render it accessible in all your controllers:
public static class ControllerExtensions
{
public static Guid GetCurrentClientID(this ControllerBase controller)
{
return (Guid)controller.ControllerContext.HttpContext.Session["ClientID"];
}
}
and now inside each controller you can access it:
public ActionResult Foo()
{
Guid id = this.GetCurrentClientID();
...
}
And if you want it to be even more globally available make it an extension method to the HttpContextBase class:
public static class ControllerExtensions
{
public static Guid GetCurrentClientID(this HttpContextBase context)
{
return (Guid)context.Session["ClientID"];
}
}
now everywhere you have access to the HttpContext (which is pretty much everywhere in an ASP.NET application) you simply use the extension method. For example inside a view:
#Html.ActionLink("foo link", "foo", new { clientid = Context.GetCurrentClientID() })
How can I have a view render a partial (user control) from a different folder?
With preview 3 I used to call RenderUserControl with the complete path, but whith upgrading to preview 5 this is not possible anymore.
Instead we got the RenderPartial method, but it's not offering me the functionality I'm looking for.
Just include the path to the view, with the file extension.
Razor:
#Html.Partial("~/Views/AnotherFolder/Messages.cshtml", ViewData.Model.Successes)
ASP.NET engine:
<% Html.RenderPartial("~/Views/AnotherFolder/Messages.ascx", ViewData.Model.Successes); %>
If that isn't your issue, could you please include your code that used to work with the RenderUserControl?
In my case I was using MvcMailer (https://github.com/smsohan/MvcMailer) and wanted to access a partial view from another folder, that wasn't in "Shared." The above solutions didn't work, but using a relative path did.
#Html.Partial("../MyViewFolder/Partials/_PartialView", Model.MyObject)
If you are using this other path a lot of the time you can fix this permanently without having to specify the path all of the time. By default, it is checking for partial views in the View folder and in the Shared folder. But say you want to add one.
Add a class to your Models folder:
public class NewViewEngine : RazorViewEngine {
private static readonly string[] NEW_PARTIAL_VIEW_FORMATS = new[] {
"~/Views/Foo/{0}.cshtml",
"~/Views/Shared/Bar/{0}.cshtml"
};
public NewViewEngine() {
// Keep existing locations in sync
base.PartialViewLocationFormats = base.PartialViewLocationFormats.Union(NEW_PARTIAL_VIEW_FORMATS).ToArray();
}
}
Then in your Global.asax.cs file, add the following line:
ViewEngines.Engines.Add(new NewViewEngine());
For readers using ASP.NET Core 2.1 or later and wanting to use Partial Tag Helper syntax, try this:
<partial name="~/Views/Folder/_PartialName.cshtml" />
The tilde (~) is optional.
The information at https://learn.microsoft.com/en-us/aspnet/core/mvc/views/partial?view=aspnetcore-3.1#partial-tag-helper is helpful too.
For a user control named myPartial.ascx located at Views/Account folder write like this:
<%Html.RenderPartial("~/Views/Account/myPartial.ascx");%>
I've created a workaround that seems to be working pretty well. I found the need to switch to the context of a different controller for action name lookup, view lookup, etc. To implement this, I created a new extension method for HtmlHelper:
public static IDisposable ControllerContextRegion(
this HtmlHelper html,
string controllerName)
{
return new ControllerContextRegion(html.ViewContext.RouteData, controllerName);
}
ControllerContextRegion is defined as:
internal class ControllerContextRegion : IDisposable
{
private readonly RouteData routeData;
private readonly string previousControllerName;
public ControllerContextRegion(RouteData routeData, string controllerName)
{
this.routeData = routeData;
this.previousControllerName = routeData.GetRequiredString("controller");
this.SetControllerName(controllerName);
}
public void Dispose()
{
this.SetControllerName(this.previousControllerName);
}
private void SetControllerName(string controllerName)
{
this.routeData.Values["controller"] = controllerName;
}
}
The way this is used within a view is as follows:
#using (Html.ControllerContextRegion("Foo")) {
// Html.Action, Html.Partial, etc. now looks things up as though
// FooController was our controller.
}
There may be unwanted side effects for this if your code requires the controller route component to not change, but in our code so far, there doesn't seem to be any negatives to this approach.
The VirtualPathProviderViewEngine, on which the WebFormsViewEngine is based, is supposed to support the "~" and "/" characters at the front of the path so your examples above should work.
I noticed your examples use the path "~/Account/myPartial.ascx", but you mentioned that your user control is in the Views/Account folder. Have you tried
<%Html.RenderPartial("~/Views/Account/myPartial.ascx");%>
or is that just a typo in your question?
you should try this
~/Views/Shared/parts/UMFview.ascx
place the ~/Views/ before your code
Create a Custom View Engine and have a method that returns a ViewEngineResult
In this example you just overwrite the _options.ViewLocationFormats and add your folder directory
:
public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
{
var controllerName = context.GetNormalizedRouteValue(CONTROLLER_KEY);
var areaName = context.GetNormalizedRouteValue(AREA_KEY);
var checkedLocations = new List<string>();
foreach (var location in _options.ViewLocationFormats)
{
var view = string.Format(location, viewName, controllerName);
if (File.Exists(view))
{
return ViewEngineResult.Found("Default", new View(view, _ViewRendering));
}
checkedLocations.Add(view);
}
return ViewEngineResult.NotFound(viewName, checkedLocations);
}
Example: https://github.com/AspNetMonsters/pugzor
Try using RenderAction("myPartial","Account");