4

I have a View which hold a list of items rendered through partials views.

I'd like to be able in the partial view to add a javascript script only once for the whole page.
I read I could use Context.Items to share data (like a boolean ScriptAlreadyIncluded) amongst all (partial) views.

But can I rely on this? Or should I use something else?


My current code (most relevant partial view being ITypedModel.cshtml and AFieldFormula_DirectFieldFormula.cshtm)

Index.cshtml

@model IEnumerable<MygLogWeb.Classes.MemberField>

@{
    Layout = "~/Views/Shared/_Layout.cshtml";

    var uid = Guid.NewGuid().ToString();
}

@using (Html.BeginForm("Index", "MemberField", FormMethod.Post, new { id = uid }))
{

    @Html.AntiForgeryToken()

    <table class="table none">
        <thead>
            <tr>
                <th>
                    @Html.DisplayNameFor(model => model.IsSystem)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.IsVirtual)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Name)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Formula)
                </th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            @Html.EditorFor(model => model)
        </tbody>
        <tfoot>
            <tr>
                <td colspan="5">
                    <input type="button" value="@Resources.Common.Buttons.add" />
                </td>
            </tr>
        </tfoot>
    </table>

    <input type="submit" value="@Resources.Common.Buttons.save"/>
}

<script type="text/javascript">
    new TableEdit("@uid");
</script>

EditorTemplates/MemberField.cshtml

@model MygLogWeb.Classes.MemberField

<tr>
    <td>
        @Html.DisplayFor(model => model.IsSystem)
        @Html.HiddenFor(model => model.IsSystem)
    </td>
    <td>
        @Html.DisplayFor(model => model.IsVirtual)
        @Html.HiddenFor(model => model.IsVirtual)
    </td>
    <td>
        @if (Model.IsSystem)
        {
            @Html.DisplayFor(model => model.Name)
            @Html.HiddenFor(model => model.Name)
        }
        else
        {
            @Html.TextBoxFor(model => model.Name, new { data_focus = "true" })
            @Html.ValidationMessageFor(model => model.Name)
        }
    </td>
    <td>
        @Html.EditorFor(model => model.Formula, "ITypedModel")
    </td>
    <td>
        @if (!Model.IsSystem)
        {
            <input type="button" value="@Resources.Common.Buttons.delete" data-code="delete" />
        }
    </td>
</tr>

EditorTemplates/ITypedModel.cshtml

...
@foreach (var item in items)
{
    <span data-refered-type="@item.Item.Value">
        @if (item.Type.IsSubclassOf(typeof(AFieldFormula)))
        {
            var newModel = item.Item.Selected ? Model : Activator.CreateInstance(item.Type);

            @Html.Partial("~/Views/MemberField/EditorTemplates/AFieldFormula_" + item.Type.Name + ".cshtml", newModel)
        }
    </span>
}

EditorTemplates/AFieldFormula_ConstantFormula.cshtml

@model MygLogWeb.Classes.ConstantFormula

<b>ConstantFormula</b>
@Html.EditorFor(model => model.Constant)
@Html.ValidationMessageFor(model => model.Constant)

EditorTemplates/AFieldFormula_DirectFieldFormula.cshtml

@model MygLogWeb.Classes.DirectFieldFormula

...Build a JSON dictionnary...

<b>DirectFieldFormula</b>
@Html.EditorFor(model => model.Field)
@Html.ValidationMessageFor(model => model.Field)

...Some controls using the JSON dictionnary...

The dictionary would be the same for every AFieldFormula_DirectFieldFormula.cshtml view, so I'd like to compute and display it only once per web page.

Serge
  • 6,554
  • 5
  • 30
  • 56

4 Answers4

2

Is Context.Items (thread) safe?

No, because HttpContext isn't thread safe, from the docs

Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.

If you want it to be rendered once you could add a hidden field into the page on the first render and check for the existence of that on subsequent renders.


Based on your comments, you should be able to use the ViewBag to mainain state across the various partial views and ensure the script is only rendered once

ITypedModel.cshtml

...
@foreach (var item in items)
{
    <span data-refered-type="@item.Item.Value">
        @if (item.Type.IsSubclassOf(typeof(AFieldFormula)))
        {
            var newModel = item.Item.Selected ? Model : Activator.CreateInstance(item.Type);
            @Html.Partial("~/Views/MemberField/EditorTemplates/AFieldFormula_" + item.Type.Name + ".cshtml", newModel, new ViewDataDictionary { { "vb", ViewBag }})
        }
    </span>
}

AFieldFormula_DirectFieldFormula.cshtml

@model MygLogWeb.Classes.DirectFieldFormula

@{ var vb = ((dynamic)ViewData["vb"]); }

@if (!vb.ScriptAlreadyIncluded)
{
    ...Build a JSON dictionnary...   
}

<b>DirectFieldFormula</b>
@Html.EditorFor(model => model.Field)
@Html.ValidationMessageFor(model => model.Field)

@if (!vb.ScriptAlreadyIncluded)
{
    ...Some controls using the JSON dictionnary...
}

@if (!vb.ScriptAlreadyIncluded)
{
    @{ vb.ScriptAlreadyIncluded = true; } // set the flag
}

This will render the script on the first call to RenderPartial("AFieldFormula_DirectFieldFormula") which then sets the ViewBag.ScriptAlreadyIncluded property to true. Then on subsequent renders (as the ViewBag is being passed in) this property will skip the JS stuff because ScriptAlreadyIncluded has already been set.

James
  • 80,725
  • 18
  • 167
  • 237
  • What do you mean by subsequent renders? – Serge Nov 05 '13 at 10:48
  • Calls to `@Html.RenderPartial(...)` after the initial one. – James Nov 05 '13 at 10:50
  • But how would a hidden field help? I can't read a hidden field from server code @{ ... } can I? – Serge Nov 05 '13 at 10:55
  • @Serge you don't need server side code though, this is a *view* issue. On the first render, insert a hidden field (i.e. check for the existence of it first using JS, if it doesn't exist, write it out and render the other JS). Then on the next render of the partial view it will find the hidden field therefore will skip any JS stuff. – James Nov 05 '13 at 11:02
  • I don't want to write all partials (about 40 instances) view with the full javascript code (a JSON dictionary of about 100 lines) then just discard all those but one on the client side. – Serge Nov 05 '13 at 11:29
  • @Serge I don't understand why you would need to write loads of partials. The point of a partial view is it can be *reused* i.e. you write it once. If each of your partials are *different* then my suggestion would be to move the general code into a separate partial and call that from inside your more specific partials. – James Nov 05 '13 at 11:34
  • I have 2 partials (in this figure) which serve to display about 40 items on my page. But they all required a specific javascript dictionnary which logics should completly be handled by the partial view (having the script load in a parent (master)page would break my "put things where they belong" rule). – Serge Nov 05 '13 at 12:12
  • @Serge if each partial has *specific* AND *conditional* JS then I can't see anyway where you won't have to write that logic in every partial anyway? To be honest, you haven't been entirely clear with your question if you update it with a code sample maybe I could answer it a bit better. – James Nov 05 '13 at 12:36
  • @Serge so looking at your update, this has nothing to do with JS at all - it seems to me you want to render the contents of a partial view once (unless there is JS inside your `AFieldFormula_DirectFieldFormula.cshtml` you have left out for brevity? – James Nov 05 '13 at 13:21
  • There is JS in AFieldFormula_DirectFieldFormula.cshtml. In order for some kind of controls (specific buttons, textboxes, ...) to work. They need now and then to gather data from the JSON dictionary (doing it in ajax would be stupid). The JSON dictionary (I call it JSON, by the end of the day it's just data available from javascript) is the same for every controls. – Serge Nov 05 '13 at 13:31
  • @Serge ok, I understand what it is you are trying to do now. Given you still need to render the *server*-side code on every call, I go back to my original solution - use pure JS for this. Let me post an example. – James Nov 05 '13 at 13:33
  • Good idea. But Actually I have to pass the ViewBag of Index.cshtml around. – Serge Nov 05 '13 at 15:42
  • @Serge no you shouldn't have to, each Partial View will get it's own `ViewBag` - you only need to pass down the one from `ITypedModel` so you can persist the property across each call to `RenderPartial`, did you try the solution? – James Nov 05 '13 at 15:43
  • You forget I want one dictionnary for the whole page. And ITypedModel is already being redenred several time (model of Index.cshtml is an IEnumerable). – Serge Nov 05 '13 at 15:46
  • @Serge if `ITypedModel` is being rendered multiple times as well then yes you will need to pass down the `ViewBag` from `Index.cshtml`, not really that much hassle though is it? – James Nov 05 '13 at 15:48
1

Is Context.Items (thread) safe?

No, as James says in his answer.

But does it need to be? If you're not doing any asynchronous processing, your request will all execute on the same thread, so thread-safety is not an issue.

And Context.Items is designed to share objects between different parts of the request processing pipeline.

Community
  • 1
  • 1
Joe
  • 122,218
  • 32
  • 205
  • 338
  • I don't know how MVC handles thread. It could have use several per request (like Microsoft does for its Sql Server technology). Yet from what you say I guess it's as simple as one request = one thread. – Serge Nov 05 '13 at 15:45
0

Based on @James' idea and this post

I could just use : ViewContext.Controller.ViewBag since I have just one Controller for the whole page (I don't use ChildActions there).

It gives:

@model MygLogWeb.Classes.DirectFieldFormula

@{
    var GlobalViewBag = ViewContext.Controller.ViewBag;
    if (GlobalViewBag.DirectFieldFormula_ScriptAlreadyIncluded as bool? != true)
    {
        <script type="text/javascript">
            // ...
        </script>
    }

    GlobalViewBag.DirectFieldFormula_ScriptAlreadyIncluded = true;
}

...
Community
  • 1
  • 1
Serge
  • 6,554
  • 5
  • 30
  • 56
0

So, this is kind of an old question, but I found it while trying to solve a recent issue.

According to Microsoft, HttpContext is not thread safe. The good news is there is a way to implement a reusable solution that would only use HttpContext in the thread controlled by ASP.NET, as recommended by Microsoft. The main limitation is this method can only keep track of scripts which were added using it, but a script created specifically to add functionality to a partial view should only be used with the partial view, anyway. See below.

Here is the original place where I left this answer. It makes use of HttpContext.Current.Items to maintain a singleton-per-request static class containing a dictionary of scripts which have already been included (using this method).

Implement an Html helper extension function which will only load a script once. (this can work for CSS and other included files, too).

Implement an extension for the HtmlHelper class and a private backing class as a singleton per HttpContext.

public static class YourHtmlHelperExtensionClass 
{
    private class TagSrcAttrTracker
    {
        private TagSrcAttrTracker() { }

        public Dictionary<string, string> sources { get; } = new Dictionary<string, string>();

        public static TagSrcAttrTrackerInstance {
            get {
                IDictionary items = HttpContext.Current.Items;
                const string instanceName = "YourClassInstanceNameMakeSureItIsUniqueInThisDictionary";

                if(!items.Contains(instanceName)) 
                    items[instanceName] = new TagSrcAttrTracker();

                return items[instanceName] as TagSrcAttrTracker;
            }
        }
    }

    public static MvcHtmlString IncludeScriptOnlyOnce(this HtmlHelper helper, string urlOfScript) 
    {
        if(TagSrcAttrTracker.Instance.sources.ContainsKey(urlOfScript))
            return null;

        TagSrcAttrTracker.Instance.sources[urlOfScript] = urlOfScript;

        TagBuilder script = new TagBuilder("script");
        scriptTag.MergeAttribute("src", urlOfScript);

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

Then, separate the JavaScript and other code into separate files.

Example .js file contents

class MyClass{
    myFunction() {
        constructor(divId){
            this._divId = divId
        }

        doSomething() {
            // do something with a div with id == divId
        }
    }
}

Example .cshtml file contents for partial view

<link rel="stylesheet" type="text/css" href="~/Content/CustomCSS/MyPartialView.css"/>

<div id="@ViewBag.id">
    Some content!  Yay!
</div>

@Html.IncludeScriptOnlyOnce("/Scripts/CustomScripts/MyPartialView.js")

Example .cshtml file that consumes the partial view

...
<body>
    <h1>Here is some content!</h1>
    @Html.Partial("/Views/MyPartial.cshtml", new ViewDataDictionary() { { "id", "id_1"} })
    @Html.Partial("/Views/MyPartial.cshtml", new ViewDataDictionary() { { "id", "id_2"} })
    @Html.Partial("/Views/MyPartial.cshtml", new ViewDataDictionary() { { "id", "id_3"} })
</body>
<script>
$().ready(
    const id1Functionality = new MyClass("id_1") // forgive the poor naming :-)
    const id2Functionality = new MyClass("id_3")
    const id3Functionality = new MyClass("id_2")

    id1Functionality.doSomething();
    id2Functionality.doSomething();
    id3Functionality.doSomething();
)
</script>

The partial view may have been included more than once and the JavaScript is packaged with the partial view, but the .js file is only included in the page once, hence no complaining by the browser that MyClass was declared more than once.

IronMonkey
  • 51
  • 8