Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prototype methods missing in IE via SignalR

I've run into a problem dealing with prototype methods disappearing (in this case Array.prototype methods) only in IE and only when the array is coming through SignalR.

I wrote a small/stupid but simple proof of concept web app that demonstrates this problem (code is all below). Notice that when you click "Update all clients" and then "Fruits containing the letter 'r'" the prototype methods in _list are missing causing an exception. In that case the array came from SignalR. Now when you click "Reset" and it resets the array to the hard-coded value the "Fruits containing the letter 'r'" button suddenly works - the prototype methods are back. Remember, this problem only occurs in IE.

HINT: When I first wrote the proof of concept I couldn't reproduce the issue. IE still had the prototype methods when the array came via SignalR but I did have another error when the page loaded. I was accidentally including jQuery twice. When I took out the redundant script to include the second jQuery it fixed that error (obviously) but now the problem could be reproduced. IE was then missing the Array prototype methods I created but only when the array comes via SignalR.

myExtensions.js:

Array.prototype.where = function (del)
{
    var ret = new Array();
    for (var i = 0; i < this.length; i++)
    {
        if (del(this[i])) ret.push(this[i]);
    }
    return ret;
}

Array.prototype.select = function (del)
{
    var ret = new Array();
    for (var i = 0; i < this.length; i++)
    {
        ret.push(del(this[i]));
    }
    return ret;
}

_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>@ViewBag.Title - My ASP.NET MVC Application</title>
        <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
        <meta name="viewport" content="width=device-width" />

        @Styles.Render("~/Content/css")

        @Scripts.Render("~/Scripts/myExtensions.js")
        @Scripts.Render("~/bundles/modernizr")
        @Scripts.Render("~/Scripts/jquery-1.7.1.js")
        @Scripts.Render("~/Scripts/jquery.signalR-1.0.0-rc1.js")
        <script src="~/signalr/hubs"></script>

    </head>
    <body>
        <header>
            <div class="content-wrapper">
                <div class="float-left">
                    <p class="site-title">@Html.ActionLink("IE/SignalR error POC", "Index", "Home")</p>
                </div>
            </div>
        </header>
        <div id="body">
            @RenderSection("featured", required: false)
            <section class="content-wrapper main-content clear-fix">
                @RenderBody()
            </section>
        </div>
        <footer>
            <div class="content-wrapper">
                <div class="float-left">
                    <p>&copy; @DateTime.Now.Year - My ASP.NET MVC Application</p>
                </div>
            </div>
        </footer>
        @*@Scripts.Render("~/Scripts/myExtensions.js")*@
    </body>
</html>

ListHub.cs

using System.Linq;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalR_Bug_POC.Hubs
{
    public class ListHub : Hub
    {
        public void RunTest()
        {
            Clients.All.updateList(new string[]
                {
                    "apple", "pear", "grape", "strawberry", "rasberry", "orange", "watermelon"
                }.Select(f => new { Name = f }).ToList());
        }
    }
}

Index.cshtml

@{
    ViewBag.Title = "Home Page";
}

@if(false)
{
    @Scripts.Render("~/Scripts/jquery.signalR-1.0.0-rc1.js")
    @Scripts.Render("~/Scripts/myExtensions.js")
    <script src="~/signalr/hubs"></script>
}

<script type="text/javascript">

    var _fruits = ["blueberry", "grape", "orange", "strawberry"].select(function (f) { return { "Name": f } });
    var _list;

    var conn = $.connection.listHub;
    $.connection.hub.start();

    conn.client.updateList = function (data)
    {
        _list = data;
        $("#theList").html("");
        for (var i = 0; i < _list.length; i++)
        {
            $("#theList").append("<li>" + _list[i].Name + "</li>");
        }
    }

    $(document).ready(function ()
    {
        $("#cmdUpdateClients").click(function ()
        {
            conn.server.runTest();
        });
        $("#cmdReset").click(function ()
        {
            conn.client.updateList(_fruits);
        });
        $("#cmdRunTest").click(function ()
        {
            var message = "";
            var fruitsContaining = _list
                .where(function (f) { return f.Name.indexOf('r') >= 0 })
                .select(function (f) { return f.Name });
            for (var i = 0; i < fruitsContaining.length; i++)
            {
                message += " - " + fruitsContaining[i] + "\n";
            }
            alert(message);
        });
        conn.client.updateList(_fruits);
    });


</script>

<input type="button" id="cmdUpdateClients" value="Update All Clients" />
<input type="button" id="cmdReset" value="Reset" />
<input type="button" id="cmdRunTest" value="Fruits containing the letter r." />
<ul id="theList"></ul>

I'm not sure if it's something I'm doing wrong in the code (i.e. something I'm doing in the wrong order) or if it's an IE bug or a SignalR bug. When I set a breakpoint for instance on the first line of the conn.client.updateList JS method and track the call stack up to the very top and see that even there (in the SignalR receive method) arrays in the 'data' object don't have my prototype methods.

like image 995
Stewart Anderson Avatar asked Oct 22 '22 21:10

Stewart Anderson


1 Answers

I encountered the same problem: when I used SignalR to pass arrays from C# to an Angular app, I couldn't use methods defined in Array.prototype on the received objects. Furthermore, the objects were indeed "array-like" in the sense that some of the array tests described here would fail. For example, arr instanceof Array would return false, but Array.isArray(arr) would return true.

The problem starts when the web application is hosted in IIS without WebSockets support. In this case, SignalR defaults in Chrome and Firefox to serverSentEvents, and in Internet Explorer and Edge to ForeverFrame.

As this question indicates, ForeverFrame is causing the arrays to be deserialized incorrectly. This is because ForeverFrame uses a different frame to maintain the SignalR connection, and arrays in different frames are created using different Array objects.

There are number of solutions here:

  1. If possible, enable WebSockets for IIS. This can be done starting from IIS 8 and Windows Server 2012.
  2. If WebSockets is not available, you can specify in the $.connection.hub.start() parameters that ForeverFrame should not be used, defaulting to LongPolling on IE and Edge.
  3. You can supply your own JSON parser to ForeverFrame, and call to window.JSON:

    $.connection.hub.json = {
        parse: function(text, reviver) {
            console.log("Parsing JSON");
            return window.JSON.parse(text, reviver);
        },
        stringify: function(value, replacer, space) {
            return window.JSON.stringify(value, replacer, space);
        }
    };
    
  4. And, as suggested in Pete's answer, you can call Array.prototype.slice on the received object, converting it into an Array of the same frame. This has to be done for any array received from SignalR, so it is not scalable like the other two options.

like image 54
Tomer Avatar answered Oct 25 '22 19:10

Tomer