49

Is it possible to await on tasks in Razor .cshtml views?

By default it complains that it can only be used in methods marked with async so I'm wondering if maybe there is a hidden switch somewhere that enables it?

Knaģis
  • 20,827
  • 7
  • 66
  • 80

8 Answers8

28

In ASP.NET Core 2.1, you can use await in Razor views.

See https://learn.microsoft.com/en-us/aspnet/core/mvc/views/partial?view=aspnetcore-2.1

Example:

@await Html.PartialAsync("../Account/_LoginPartial.cshtml")
Kirill Rakhman
  • 42,195
  • 18
  • 124
  • 148
2

I've wanted something like this for a long time - a lot of the pages we write could be thrown together by a Jr Dev if they didn't have to write a bunch of queries; and, it's the same basic query boilerplate every time anyway - why should they have to write them for each Controller, when the majority of their work is to get content up? I use C# so I don't have to deal with memory management, why should an HTML coder have to deal with query details?

There is a trick you can use to sort of implicitly load data async into the View. First, you define a class that expresses what data you want. Then, at the top of each View, instantiate that class. Back in the Controller, you can lookup the View you know you're going to use, open it, then compile that class. You can then use it to go get the data the View will need, async, in the Controller the way MVC enforces. Finally, pass it off with a ViewModel to the View as MVC prescribes, and, through some trickery - you have a View that declares what data it's going to use.

Here's a StoryController. Jr Devs write stories as simple .cshtml files without having to know what a Controller, database or LINQ is:

public class StoryController : BaseController
{
    [OutputCache(Duration=CacheDuration.Days1)]
    // /story/(id)
    public async Task<ActionResult> Id(string id = null)
    {
        string storyFilename = id;

        // Get the View - story file
        if (storyFilename == null || storyFilename.Contains('.'))
            return Redirect("/");   // Disallow ../ for example

        string path = App.O.AppRoot + App.HomeViews + @"story\" + storyFilename + ".cshtml";
        if (!System.IO.File.Exists(path))
            return Redirect("/");

        return View(storyFilename);

All this does for now is go get the View file based on the URL, allowing something like WebForms (except inside MVC and using Razor). But we want to show some data - in our case, people and projects that accumulate in the database - with some standard ViewModels and Partials. Let's define how and compile that out. (Note that ConservX happens to be the core Project namespace in my case.)

    public async Task<ActionResult> Id(string id = null)
    {
        string storyFilename = id;

        // 1) Get the View - story file
        if (storyFilename == null || storyFilename.Contains('.'))
            return Redirect("/");   // Disallow ../ for example

        string path = App.O.AppRoot + App.HomeViews + @"story\" + storyFilename + ".cshtml";
        if (!System.IO.File.Exists(path))
            return Redirect("/");

        // 2) It exists - begin parsing it for StoryDataIds
        var lines = await FileHelper.ReadLinesUntilAsync(path, line => line.Contains("@section"));

        // 3) Is there a line that says "new StoryDataIds"?
        int i = 0;
        int l = lines.Count;
        for (; i < l && !lines[i].Contains("var dataIds = new StoryDataIds"); i++)
        {}

        if (i == l) // No StoryDataIds defined, just pass an empty StoryViewModel
            return View(storyFilename, new StoryViewModel());


        // https://stackoverflow.com/questions/1361965/compile-simple-string
        // https://msdn.microsoft.com/en-us/library/system.codedom.codecompileunit.aspx
        // https://msdn.microsoft.com/en-us/library/system.codedom.compiler.codedomprovider(v=vs.110).aspx
        string className = "__StoryData_" + storyFilename;
        string code = String.Join(" ",
            (new[] {
                "using ConservX.Areas.Home.ViewModels.Storying;",
                "public class " + className + " { public static StoryDataIds Get() {"
            }).Concat(
                lines.Skip(i).TakeWhile(line => !line.Contains("};"))
            ).Concat(
                new[] { "}; return dataIds; } }" }
            ));


        var refs = AppDomain.CurrentDomain.GetAssemblies();
        var refFiles = refs.Where(a => !a.IsDynamic).Select(a => a.Location).ToArray();
        var cSharp = (new Microsoft.CSharp.CSharpCodeProvider()).CreateCompiler();
        var compileParams = new System.CodeDom.Compiler.CompilerParameters(refFiles);
        compileParams.GenerateInMemory = true;
        compileParams.GenerateExecutable = false;

        var compilerResult = cSharp.CompileAssemblyFromSource(compileParams, code);
        var asm = compilerResult.CompiledAssembly;
        var tempType = asm.GetType(className);
        var ids = (StoryDataIds)tempType.GetMethod("Get").Invoke(null, null);

        using (var db... // Fetch the relevant data here

        var vm = new StoryViewModel();
        return View(storyFilename, vm);
    }

That's the majority of the work. Now Jr Devs can just declare the data they need like so:

@using ConservX.Areas.Home.ViewModels.Storying
@model StoryViewModel
@{
    var dataIds = new StoryDataIds
    {
        ProjectIds = new[] { 4 }
    };

    string title = "Story Title";
    ViewBag.Title = title;
    Layout = "~/Areas/Home/Views/Shared/_Main.cshtml";
}
@section css {
...
Chris Moschini
  • 36,764
  • 19
  • 160
  • 190
1

I landed on this question because I am a newbie to Razor and I wanted to display a simple "loading..." screen while my Controller Code was calculating data.

So I found this link: https://www.codeproject.com/Articles/424745/MVC-Razor-In-Progress-Icon which was helpful, but because I was a total novice at Razor, I was unable to make this work.

What finally worked for me was the following.

1) Add the "loading" div as suggested in the code project to my .cshtml file:

<div id="divLoading" style="margin: 0px; padding: 0px; position: fixed; right: 0px;
    top: 0px; width: 100%; height: 100%; background-color: #666666; z-index: 30001;
    opacity: .8; filter: alpha(opacity=70);display:none">
    <p style="position: absolute; top: 30%; left: 45%; color: White;">
        Loading, please wait...<img src="../../Content/Images/ajax-loading.gif">
    </p>
</div>

2) Modify my Razor form from

<input type="submit" value="Go"/>

to

<input type="button" value="Go" onclick="JavascriptFunction()" />

3) Create the JavascriptFunction() in my .cshtml page:

<script type="text/javascript" language="javascript">
    function JavascriptFunction() {
        $("#divLoading").show();
        $('form').submit();
    }
</script>

If I understand all of the above correctly, what this does is execute the function JavascriptFunction when I press the Go button.

The JavascriptFunction does 2 things: 1) Change the view of the page by showing the previously hidden (display:none) divLoading div. 2) Submit all the forms on this page (I only have one, so it submits the form the same as if I had they type submit on the button)

After the Controller launched by the form submit is done, it loads a new view on a new page, and the initial page (and the "loading" div) is gone. Mission accomplished.

Owen Ivory
  • 244
  • 1
  • 9
1

You can await calls in razor pages? I have a Blazor app and most of my methods are async:

Razor page:

<MatFAB Icon="@MatIconNames.Autorenew" Style="transform:scale(0.8); background:#333;"
                OnClick="@(async () => await OnInitializedAsync())"></MatFAB>

This is a MatBlazor FloatingActionButton which calls the life time cycle event OnInitializedAsync()

C# Code:

protected override async Task OnInitializedAsync()
{
    // Do something like get data when the form loads
}
James Heffer
  • 686
  • 1
  • 6
  • 17
0

No, that's not possible and you shouldn't need to do it anyway. Razor views should contain markup and at most some helper call. async/await belongs to your backend logic.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 32
    Downvote for the "Don't do this" part. Rendering first bytes before data arrives isn't strange, nor are View-dictated data needs. For example, if I handed an intern Razor and they needed to throw pages together without knowing how to call the DB, I could teach them to call for data via a lazy-loading repo with a really simple interface - except you run into this wall of async (possibly) being restricted in Razor. – Chris Moschini Sep 17 '14 at 18:30
  • 2
    @ChrisMoschini that just doesn't make any sense at all. "view-dictated data need" is what a ViewModel is and it's already provided by the controller. "rendering before data arrives" is just a looking at the slow data retrieval problem the wrong way. there is caching for that. – Sedat Kapanoglu Oct 01 '14 at 19:46
  • 1
    @ssg Consider a page that's different for everyone, every time they visit, and it takes some time to calculate the results. Suppose it's a list of stuff. Better to render, unbuffered, to the outputstream what you have as soon as you have it, than to make the user wait until absolutely everything has arrived. The ViewModel approach would get you the crappy latter outcome. Letting the View sip and render data in real-time gives you the better, low-time-to-first-result former outcome. – Chris Moschini Oct 02 '14 at 02:47
  • 1
    @ChrisMoschini you confuse browser-side processing with server-side processing. A View is supposed to work on the finished data after all asynchronous operations have finished in the controller. It's the *controller's* job to get the data, not the view's. If you want partial rendering, structure your HTML and Javascript so that rendering starts before all data was downloaded. If you want the two things separate, use ODATA or Web API to get the Json. What you are describing is more-or-less the way Web Forms worked. – Panagiotis Kanavos Oct 10 '14 at 07:08
  • 5
    @PanagiotisKanavos Incorrect. When a Response begins, it doesn't instantly transfer all its contents to the client. Instead, it streams over time. You can render gradually from the server as the data becomes available if you deliver your View in an unbuffered way - but the standard approach to ASP.Net MVC/Razor makes this either difficult or impossible. Going and fetching more data via js after the fact is something entirely different, and not to be confused with streaming a single response over the course of several seconds, without the need for additional requests. – Chris Moschini Oct 13 '14 at 04:00
  • @ChrisMoschini what you describe is the client's way of working which is unrelated to whether the server uses one or more threads to process a request and whether it yields on blocking operations. MVC doesn't make it difficult, it simply doesn't deal with it at all. If you want to pull different contents from the server, go ahead and call multiple actions or multiple controllers. Razor doesn't prevent this at all. `async` in ASP.NET has to do with how the server releases the processing thread whenever a blocking operation is encountered, and resumes when it finishes – Panagiotis Kanavos Oct 13 '14 at 06:53
  • 1
    @PanagiotisKanavos Still no. Reread the third comment here. Server begins sending response before it knows all the content it will emit. Threads in background meanwhile fetch. When they arrive with results server finishes response. Improves time to first byte. – Chris Moschini Oct 13 '14 at 16:43
  • 1
    I'm implementing @Html.Translate(key) helper, that contains await method inside to get database value. That is example of real world "view-dictated data need" beahviour. So I need it to do anyway. – Alexey Ryazhskikh Sep 07 '15 at 13:30
  • While I agree that there shouldn't be any real need for async in Razor views within the context of an AspNet MVC site, there is a use case for this in AspNet Web Pages (i.e. Non-MVC, Razor only) site. I'm encountering this on a pure HTML site that I converted over to WebPages and added a couple of features that makes some async API calls. – Hallmanac Jun 17 '17 at 18:31
  • 1
    Another downvote for "Don't do this". He didn't ask for advice on best MVC practices, he wants to perform a specific task. You don't know his use case and his platform, this may be his only option in the situation he is in so stick to answering factually. – Tony Cheetham Dec 18 '19 at 17:32
0

If you really need it, you can do this, it will be ugly, but it will work.

In View

@{  
var foo = ViewBag.foo;
var bar = ViewBag.bar;
}

In Controller

public async Task<ActionResult> Index()
        {
            ViewBag.foo = await _some.getFoo();
            ViewBag.bar = await _some.getBar();
            return View("Index");
        }
TheZodchiy
  • 136
  • 2
  • 11
  • This is equivalent to var foo = await... bar = await ... return View("Index", new { foo=foo, bar=bar }) - you're still just loading data via await/async in the Controller as normal, not in Razor. – Chris Moschini Dec 09 '17 at 03:46
0

Following on MaxP's answer, it's easy to return a value from that code, despite Knagis comment:

@{
    int x = DoAsyncStuffWrapper().Result;
}
@functions {
    async Task<int>DoAsyncStuffWrapper() 
    {
        await DoAsyncStuff();
    }
}
Kevin McDowell
  • 532
  • 6
  • 20
-1

I know this is an older thread, but I'll add my input just in case someone else finds it useful. I ran into this problem working with the new MongoDB driver in ASP.Net MVC - the new driver (for now), only implements async methods and returns async cursors, which can't be used in a foreach because asynccursor doesn't implement IEnumerable. The sample code typically looks like:

while(await cursor.movenextasync)
    var batch=cursor.current
    foreach(var item in batch)
        --do stuff here--

But, this doesn't work in razor, because views are inherently not async, and await doesn't cut it.

I got it to work by changing the first line to:

while(cursor.MoveNextAsync().Result)

which returns true until the cursor hits the last entry.

Hope that helps!

EricF
  • 173
  • 7
  • 1
    The correct answer is "You are doing it wrong". This should be done in an *asynchronous way* in the controller's method. Trying to talk from the view directly to a database,bypassing both the controller, model and any data layer is the exact opposite of MVC and simply causes delays in rendering. The view should just render the data in a view model. The view model should only contain the data needed for that view. It's the *controller's* job to retrieve the data and constuct the DTO/model and send it to the view – Panagiotis Kanavos Mar 30 '16 at 11:04
  • @PanagiotisKanavos Wouldn't that mean that for a large listing retrieved one-by-one piece from database the rendering and actually sending the output to the client would not occur until the full list is loaded to memory? It definitely makes sense to have logic in controller, however for the case I mentioned, wouldn't it be better to be able to pass some sort of async enumerable, and render it piece-by-piece asynchronously in view? Or is there some technical limitation for that? – martinh_kentico Jun 30 '17 at 06:35
  • @martinh_kentico no, not at all. If you are loading entries one by one there is a serious problem in the data access logic. *Why* load one by one when a query will return *all* results at once? Why return *a lot* of results when you *can't* display them? Why not use *paging* in this case? Why not use a `.Contains()` clause in LINQ, or `IN (..)` in SQL to pass a list of IDs? – Panagiotis Kanavos Jun 30 '17 at 07:41
  • @martinh_kentico even worse - what would be the point ot an async operation in the view? You can't send the HTML response to the browser until all of this finishes anyway. If you want to fill parts of a page when data becomes available you *have* to use AJAX on the browser, or really old-style frames. – Panagiotis Kanavos Jun 30 '17 at 07:45
  • You don't query the data one by one, you get DataReader for the query and stream the data directly from DB if you work with larger amount of data. Also, the output is s stream, so why would you need to wait for the whole page to be rendered before starting to send the data to client? If WebForms is capable of streaming output as it renders, why MVC couldn't? Also, if you work with component-based models, having more components which call external services would mean that you need to initiate those external calls sequentially. – martinh_kentico Jul 01 '17 at 10:09
  • I would expect that I could call async method (e.g. child action, or async enumerator), which would initiate necessary async calls. Until those would be finished the rendering of the remainder of the view could continue and raise other async actions, and by the time the output is available the engine would be able to send that particular data in order to the client. All it would need would be wrapping the list of output blocks into tasks. – martinh_kentico Jul 01 '17 at 10:16