5

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.

Stewart Anderson
  • 333
  • 2
  • 14

2 Answers2

3

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.

Community
  • 1
  • 1
Tomer
  • 1,606
  • 12
  • 18
0

With the following modification, it works just fine for me:

var fruitsContaining = _list
    .where(function (f) { return f.indexOf('r') >= 0 })
    .select(function (f) { return f});

The where and select were actually there, it was f.Name that wasn't, as the array members are strings.

Update:

Okay, ignore the above. Here's the fix:

var list = list
if (navigator.appName == 'Microsoft Internet Explorer') {
    list = Array.prototype.slice.call(_list);
}
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";
}

I don't entirely understand the issue, but I believe it may be a bug in jquery. Though the question is a bit different, I stole the solution from this question: Why does this change to the Array prototype not work in my jQuery plugin?

Additional Update

Added the IE check.

Community
  • 1
  • 1
Pete
  • 6,585
  • 5
  • 43
  • 69
  • Actually .Name is there. Look at the initialization of _list - the select method creates .Name on each element of the array. Same happens server-side in the hub. Each element is an object - not a string. That's why it works on all browsers except for IE when coming through SignalR. Even when IE is looking at the _list that was initialized in the JS it works fine. Also I can see the elements and the methods of the objects in the developer tools of IE and Chrome so I know for a fact .Name is there but in IE when the data comes via SignalR the data is there but the prototype methods are not. – Stewart Anderson Dec 28 '12 at 15:30
  • I probably should have put the .select call on the initialization of _list on the next line so it would be more easily visible on this narrow of text. – Stewart Anderson Dec 28 '12 at 15:32
  • CORRECTION: look at the init of _fruits - it has the .select that creates .Name but _fruits gets passed to the update method that sets _list to _fruits... So .Name is there but prototype methods in IE when data comes via SignalR is not. – Stewart Anderson Dec 28 '12 at 15:41
  • Okay. I see what's happening now. I was misusing your sample. Very strange behavior. I'll look into it some more. – Pete Dec 28 '12 at 15:51
  • Here are a few notes. I still don't completely understand, but I believe it's not returning an actual array, but an "array-like object". See this question for more information: http://stackoverflow.com/questions/6599071/array-like-objects-in-javascript Why that works in other browsers but not IE, I'm not sure, but the explanation of why it shows up as an array in the display is explained in that question. – Pete Dec 28 '12 at 17:09
  • This work around works! Thank you very much Pete! I've up-voted it BUT since it is a work around and not a solution, before I mark it as the answer I want to see if anyone has a solution for the problem. – Stewart Anderson Jan 03 '13 at 01:39
  • While I would agree that this is a workaround, there is no fix from what I've read. The reason is that it is not an array. It's an array-like object (after it comes back from jQuery). Firefox and Chrome will treat it as an array, but IE makes a distinction and will not. – Pete Jan 03 '13 at 02:10