MVC executing view code before layout code and rui

2019-04-26 18:17发布

I'm trying to move all of my javascript includes to the bottom of the page. I'm using MVC with Razor. I've written a helper method to register scripts. It keeps the scripts in the order they're registered and it excludes dupes.

@{ Html.RegisterScript("/scripts/someFile.js"); }

And then right before the closing body tag I do something like this:

    @Html.RenderScripts()
</body>

This all works fine if the scripts are registered all on one page. If I register some scripts in my layout, and some scripts in my inner view, things go awry. Basically, the inner view is executing before the layout. So event though the scripts in the inner view depend on the scripts in the outer view, they're being registered prior.

So if I have _Master.cshstml and it has

@{
   Html.RegisterScript("script1.js");
   Html.RegisterScript("script2.js");
}

And then I have InnerView.cshtml with _Master.cshtml as its layout and in the view I have

@{
   Html.RegisterScript("script3.js");
}

The scripts get rendered out in this order: script3, script1, script 2.

Is there any way I can force the layout to execute before the inner view? I've tried moving things all over the place and everything the inner view always seems to execute first. Perhaps this is expected? If so, what is the best way for me to accomplish what I'm trying to do?

6条回答
我欲成王,谁敢阻挡
2楼-- · 2019-04-26 19:00

There is no way to change the order that MVC renders the pages (as far as I know) but what you can try to do is update your render script functionality to denote when a layout is having its scripts registered. So for instance your layout may be:

@{
   Html.StartLayoutScriptRegistration();
   Html.RegisterScript("script1.js");
   Html.RegisterScript("script2.js");
}

This would trigger to your system that the scripts registered after StartLayoutScriptRegistration() is called should be rendered first, then followed by your view's script registrations registered prior to the 'StartLayoutScriptRegistration()' being called..

查看更多
孤傲高冷的网名
3楼-- · 2019-04-26 19:05

Code placed in @section parts always get executed in your expected order, so you could make use of that fact:

In your _Master.cshtml write:

@{
   Html.RegisterScript("script1.js");
   Html.RegisterScript("script2.js");
}

@RenderSection("references", required: false)

And in your InnerView.cshtml:

@section references {@{
       Html.RegisterScript("script3.js");
}}

This will lead to your desired include order: script1, script2, script3

Notes: Since the "references" section will only consists of one code block and no real Html output, you could place the @RenderSection part anywhere in _Master.cshtml. This will work with nested layouts too!

查看更多
萌系小妹纸
4楼-- · 2019-04-26 19:11

No, that's the order that they execute, from inner-most to outer-most. I would recommend using some kind of LIFO (last in first out) collection like Stack<T> to hold the scripts in and then pop them off the stack to render them out; this way, the scripts added last i.e. the ones in the layout, will be the ones that are rendered first.

EDIT

I wrote a nuget package that handles rendering scripts from Partial views and Templates that solves this problem.

查看更多
成全新的幸福
5楼-- · 2019-04-26 19:12

First, create 2 HTML helpers. One for adding scripts by priority:

    public static MvcHtmlString AddScript(this HtmlHelper htmlHelper, string script, int executionSequence = int.MaxValue)
    {
        Dictionary<int, List<string>> scripts = new Dictionary<int, List<string>>();
        if (htmlHelper.ViewContext.HttpContext.Items["_scripts_"] != null)
        {
            scripts = htmlHelper.ViewContext.HttpContext.Items["_scripts_"] as Dictionary<int, List<string>>;
        }

        if (!scripts.ContainsKey(executionSequence))
        {
            scripts.Add(executionSequence, new List<string>());
        }
        string tag = string.Format("<script type=\"text/javascript\" src=\"{0}\"></script>", script);
        scripts[executionSequence].Add(tag);
        htmlHelper.ViewContext.HttpContext.Items["_scripts_"] = scripts;
        return MvcHtmlString.Empty;
    }

And one for writing those scripts to your view:

    public static IHtmlString RenderScripts(this HtmlHelper htmlHelper)
    {
        StringBuilder sb = new StringBuilder();
        foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
        {
            if (key.ToString() == "_scripts_")
            {
                Dictionary<int, List<string>> scripts = htmlHelper.ViewContext.HttpContext.Items[key] as Dictionary<int, List<string>>;
                foreach (int index in scripts.Keys.OrderBy(x => x))
                {
                    foreach (string script in scripts[index])
                    {
                        if (script != null)
                        {
                            sb.AppendLine(script);
                        }
                    }
                }
            }
        }

        return MvcHtmlString.Create(sb.ToString());
    }

Then add the scripts to your partial views:

@Html.AddScript("script2.js", 2)
@Html.AddScript("script1.js", 1)

And finally, render the scripts in your Razor template:

@Html.RenderScripts()

The results will be sorted by priority and rendered inside script tags:

<script type="text/javascript" src="script1.js"></script>
<script type="text/javascript" src="script2.js"></script>
查看更多
贼婆χ
6楼-- · 2019-04-26 19:18

I ended up solving this problem by keeping track of the scripts per view. Basically I keep a

Dictionary<string, SortedList<int, string>> registeredScripts;

and a

 SortedList<int, string> viewOrder;

Every time I register a script I pass in the script path and the view name (retrieved from ViewDataContainer.ToString()). I keep track of the order in which I've seen the views in the viewOrder object. I then use the dictionary to keep track of the scripts per view. The key is the view name. When it comes time to render everything out, I reverse the viewOrder SortedList<> and write out all the scripts by view. The nice bit is, the scripts are in order by view, and the view order once reversed is correct.

I did a horrible job explaining that, but for copyright reasons I cannot share the code.

查看更多
等我变得足够好
7楼-- · 2019-04-26 19:18

As an accurate solution for script depedencies, you can provide some helper methods to:

Specify dependencies

Html.RegisterScript("script3.js", depsOn: new string[] {"script1.js", "script2.js"});

Specify relative priority:

Html.RegisterScript("script3.js", Position.Bottom);
Html.RegisterScript("script1.js", Position.Top);
Html.RegisterScript("script2.js", Position.Top);

This way you have the control needed in most situations.

查看更多
登录 后发表回答