11

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 "";
    }
}
Dr. Roggia
  • 1,095
  • 3
  • 16
  • 40
  • 1
    Yes do this in your controller! True MVC (not Microsoft's interpretation of it) says no logic should ever be in the view (a views job is to render the view model, no more). This is a good reason why not. – Liam Oct 06 '17 at 11:06

3 Answers3

4

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>
Dr. Roggia
  • 1,095
  • 3
  • 16
  • 40
  • This isn't the same, though - in a `.cshtml` page, code in the `@functions` block has full access to the current `RazorPage` instance via `this`, but in your approach the `Functions` class has no access to that data at all, requiring it all to be passed as parameters. – Dai Feb 05 '22 at 07:45
3

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.

MichaelLake
  • 1,735
  • 14
  • 17
  • Thanks for the response. I did it and it seems to work. The only problem is that '@Html.TextBox(..)' gives me some error when using inherits – Dr. Roggia Oct 06 '17 at 12:03
  • Hey - I have updated my answer so you can pass the model type. Hopefully that is enough to fix the problem. – MichaelLake Oct 06 '17 at 12:44
  • Thanks from me too, I never knew this was possible. On further research I can also recommend this answer (and the link that it contains): https://stackoverflow.com/a/23854627/1220550 – Peter B Oct 06 '17 at 12:53
  • Thank you @MichaelLake for the help, is there any way to use the '@inject' instead of the inherit? Because i don't want to change all my views, because i'm using a model on each. I tried by myself with the inject and creating an abstract object, but it gives me an error because i did not included it on my services on the startup. Any tips for that? – Dr. Roggia Oct 06 '17 at 14:05
3

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>.

  1. In your project, subclass Microsoft.AspNetCore.Mvc.Razor.RazorPage or Microsoft.AspNetCore.Mvc.Razor.RazorPage<TModel>, e.g. class MyPage<TModel> : RazorPage<TModel>
  2. Add whatever methods you like (protected or public, and static or instance all work).
  3. 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( ... );
        }
    }
}
Dai
  • 141,631
  • 28
  • 261
  • 374