1
votes

I'm trying to resolve an issue between, what I perceive is, AJAX and Server Sent Events. I have an application that does a post with some instructions to the controller, and I would like the controller to send some commentary back as an event to let the user know that the action requested has been performed (can have errors or take a while).

The idea is that the user can send a package of different instructions through the client, and the server will report through SSE when each of these actions are completed.

The problem I see through Fiddler is that when the post is performed, the response that it gets back contains my eventsource message that I would like used. However, the eventsource code also appears to call a GET, in which it appears to want that eventsource message. Because it doesn't get that, the connection repeatedly closes.

I currently have some controller code like so:

    [System.Web.Http.HttpPost]
    public void Stop(ProjectViewModel model)
    {
        ProjectManager manager = new ProjectManager();
        if (model.Servers != null && model.Servers.Count != 0)
        {
            string machine = model.Servers[0];
            foreach (string service in model.Services)
            {
                manager.StopService(service, machine);
                Message("stop", service);
            }
        }
    }

and in my view, both Ajax/XHR and server sent events set up like so:

var form = document.getElementById("submitform");

    form.onsubmit = function (e) {
        // stop the regular form submission
        e.preventDefault();

        // collect the form data while iterating over the inputs
        var data = {};
        for (var i = 0, ii = 2; i < ii; ++i) {
            var input = form[i];
            if (input.name == "Servers") {
                data[input.name] = document.getElementById("ServerSelect").options[document.getElementById("ServerSelect").selectedIndex].text;
            }
            else if (input.name == "Services")
                data[input.name] = document.getElementById("ServiceSelect").options[document.getElementById("ServiceSelect").selectedIndex].text;
        }
        if (action) { data["action"] = action };

        // construct an HTTP request
        var xhr = new XMLHttpRequest();
        if (action == "stop") {
            xhr.open(form.method, '/tools/project/stop', true);
        }

        if (action == "start") {
            xhr.open(form.method, '/tools/project/start', true)
        }

        xhr.setRequestHeader('Content-Type', 'application/json; charset=urf-8');



        // send the collected data as JSON
        xhr.send(JSON.stringify(data));

        xhr.onloadend = function () {
            // done
        };
    };

    function events() {
        if (window.EventSource == undefined) {
            // If not supported  
            document.getElementById('eventlog').innerHTML = "Your browser doesn't support Server Sent Events.";
        } else {
            var source = new EventSource('../tools/project/Stop');
            source.addEventListener("message", function (message) { console.log(message.data) });

            source.onopen = function (event) {
                document.getElementById('eventlog').innerHTML += 'Connection Opened.<br>';
                console.log("Open");
            };

            source.onerror = function (event) {
                if (event.eventPhase == EventSource.CLOSED) {
                    document.getElementById('eventlog').innerHTML += 'Connection Closed.<br>';
                    console.log("Close");
                }
            };

            source.onmessage = function (event) {
                //document.getElementById('eventlog').innerHTML += event.data + '<br>';
                var newElement = document.createElement("li");
                newElement.textContent = "message: " + event.data;
                document.getElementById("eventlog").appendChild(newElement)
                console.log("Message");
            };
        }
    };

I'm somewhat new to web development, and I'm not sure how to resolve this issue. Is there a way I can have the eventsource message read from that POST? Or have it sent to the GET instead of being sent as a response to the POST? Overall, it seems that the most damning issue is that I can't seem to get the event messages sent to the GET that is requested by the eventsource api.

EDIT: Since posting this, I tried creating a new method in the controller that specifically handles eventsource requests, but it appears that the event response still somehow ends up in the POST response body.

        public void Message(string action, string service)
    {
        Response.ContentType = "text/event-stream";
        Response.CacheControl = "no-cache";
        //Response.Write($"event: message\n");
        if (action == "stop")
        {
            Response.Write($"data: <li> {service} has stopped </li>\n\n");
        }
        Response.Flush();
        Thread.Sleep(1000);
        Response.Close();
    }
1
Why Response.Close(); inside foreach block, since it ends response?Abdelrahman M. Allam
@AbdelrahmanM.Allam Sorry, that has since been removed, but either way it didn't seem to be affecting behavior. I'll update the code.nostalgk
Ok, lets moving using Response.Close(); within if(action == 'stop') block onlyAbdelrahman M. Allam
Yes, that is the case. No specific reason, just figured it'd be cleaner to close the response (especially during debug sessions) after the work has been done.nostalgk

1 Answers

0
votes

I ended up solving this. My original idea was to pass the viewmodel in each of my methods back and forth with a Dictionary<string,string> to key in each event that can be used, but the viewmodel is not persistent. I solved this issue further by implementing the events in a Dictionary saved in Session data, and the usage of Sessions for MVC can be found in the resource here that I used:

https://code.msdn.microsoft.com/How-to-create-and-access-447ada98

My final implementation looks like this:

public void Stop(ProjectViewModel model)
    {
        ProjectManager manager = new ProjectManager();
        if (model.Servers != null && model.Servers.Count != 0)
        {
            string machine = model.Servers[0];
            foreach (string service in model.Services)
            {
                manager.StopService(service, machine);
                model.events.Add(service, "stopped");
                this.Session["Events"] = model.events;
            }
        }
        //return View(model);
    }

public void Message(ProjectViewModel model)
    {
        Thread.Sleep(1000);
        Response.ContentType = "text/event-stream";
        Response.CacheControl = "no-cache";
        Response.AddHeader("connection", "keep-alive");
        var events = this.Session["Events"] as Dictionary<string, string>;
        Response.Write($"event: message\n");
        if (events != null && events.Count != 0)
        {
            foreach (KeyValuePair<string, string> message in events)
            {
                Response.Write($"data: {message.Key} has been {message.Value}\n\n");
            }
        }
        Response.Flush();
        Thread.Sleep(1000);
        Response.Close();

    }

Adding keep-alive as connection attribute in the HTTP Response header was also important to getting the SSEs to send, and the Thread.Sleep(1000)'s are used due to the stop action and message action happening simultaneously. I'm sure there's some optimizations that can go into this, but for now, this is functional and able to be further developed.