MVC code to see whether or not a Controller is still in use
In my MVC application, I have a controller which creates an instance of a Model
object. Within the Model
, I have a timer which executes a function every 700ms
. When I run the controller within my website, I continue to see the timer running even aft开发者_开发问答er I close the browser window. (The timer starts/stops a serial port connection, and I have a light that indicates when there is communication).
The communication actually never stops until I restart IIS entirely.
Is there some way in which I can check whether the controller is still in use, and thereby stop the timer if the controller is no longer active?
Here is the model code:
public CheckPulses(int interval) {
CheckPulsesTimer(interval);
}
public void CheckPulsesTimer(int interval) {
Timer timer = new Timer();
timer.Enabled = true;
timer.AutoReset = true;
timer.Interval = interval;
timer.Elapsed += new ElapsedEventHandler(GetPulses);
}
And here is the Controller code:
public ActionResult Index() {
CheckPulses Pulses = new CheckPulses(700);
return View();
}
I thought perhaps I should just add something to the GetPulses
event, which happens every time the timer interval expires, and check there whether the controller is still in use. If it isn't, stop the timer. However, I'm not sure how to write that check. Can anyone point me in the right direction?
I think the core issue here is in your understanding of what a controller being "in use" is.
It seems as if you are imagining the following:
- The user types in your URL and the browser connects to the server.
- The server routes the request to an MVC Controller, which keeps a connection to the browser, and responds to each subsequent request using this connection.
- The user closes their browser, or navigates away - and the Controller closes down the connection, and disposes.
This is not, however, what actually happens. Web traffic is generally a series of isolated, individual request/response pairings. There is no ongoing connection between the browser and the server.
The more correct sequence is something like the following:
- The user types in your URL, and the browser connects to your server on port 80 (or 443 for https), passing in a valid HTTP request, containing a URL, HTTP verb, and other info.
- Your server uses the HTTP request's information to route the request to a specific piece of executable code (in this case, your
Controller
's specificAction
method). - In order to execute, the server creates an Instance of your
Controller
, then fires it'sAction
method, using the HTTP request's information as parameters. - Your
Controller
code executes, and an HTTP response message is sent back to the browser. - The
Controller
is.Dispose()
'd by theControllerFactory
.
The issue you're experiencing is not caused by your Controller
being "active" - because that's not something Controller
s do. You are, instead, creating a Timer
within your Model
- and the Model
, therefore, continues to live on in memory, unable to be killed by GarbageCollection
. See this answer for more details.
In essence, think of it like this:
Your Controller
is acting as a factory. When you call its Index
method, it's equivalent to telling the factory "produce 1 Model
". Once the Model
rolls out of the factory, the factory is free to shut down, turn off the lights, and send all the workers home (the Controller
is .Dispose()
'd).
The Model
, however, lives on in memory. In most cases, the Model
should probably die off due to GarbageCollection
once it goes out of scope - but because you have an active Timer
inside of it, something is preventing that process from executing - and so the Model
continues to live in your server's memory, happily firing its code again and again on each timer expiration.
Note that this has nothing to do with whether or not the user is still on your web-site, or whether their browser is still open, etc. It has to do with the nature of variable scoping and GarbageCollection
on your server. You have now started a process which will continue until told to stop.
A potential solution:
In order to fix this, you might consider the following:
Create your
Timer
inside of a Singleton object. Something like:public static class ConnectionManager { private static bool initialized = false; private static DateTime lastStartedAt = DateTime.Now; private static Timer timer = null; private static void Initialize() { if (timer == null) { timer = new Timer() { AutoReset = true, }; timer.Elapsed += new ElapsedEventHandler(GetPulses); } } public static void Start(int interval) { lastStartedAt = DateTime.Now; Initialize(); // Create a timer if necessary. timer.Enabled = true; timer.Interval = interval; } public static void Stop() { timer.Enabled = false; } }
Change your
Controller
to something like this:public ActionResult Index() { ConnectionManager.Start(700); return View(); }
When you handle the expiration of the
Timer
event, check to see how long ago thelastStartedAt
event occurred. If it is more thanX
minutes, do not process, and fireConnectionManager.Stop()
.
This will give you the ability to keep your serial activity polling on a rolling expiration. In other words - each time a user requests your Index
page, you will refresh the timer, and ensure that you are listening. After X
minutes, though, if no users have made a request - you can simply shut down the timer and the associated port.
You are likely not stopping or disposing the Timer. This will cause it to not be garbage collected and stay active for the duration the application stays running.
You should stop and dispose of the Timer within the Controller's Dispose method.
You have several options:
- Override Dispose method on the controller and dispose your model with calling Pulses.Dispose()
- Override OnActionExecuted method on the controller and do disposing
- Create custom action filter and implement OnActionExecuted method and assign it to controller or action.(You can place reference to your model into TempData and then in your ActionFilter check if filterContext.Controller.TempData["MyReferenceToModel"] != null, then use it to dispose timer)
- Implement IDisposable interface in your Model and use it inside using statement
Hope this helps, Dima
You can stop the Timer with JQuery+ MVC.
Jquery can respond to the unload event of the page like so:
$(window).unload(function() {
alert('Handler for .unload() called.');
});
You can make ajax calls to your controllers. We also need to create a property to access the timer. I'll assume you called your Timer Property MyTimer
Timer MyTimer { get; set; }
So you need to create a controller that returns void and calls the Stop() method of your Timer. [HttpPost] public void KillTimer() { MyTimer.Stop(); }
Now if you add an Ajax call to your unload event we created earlier, then we should be able to kill the timer whenever the page closes.
$.ajax(
{
type: "POST",
url: "/Home/Index",
,
success: function(result)
{
alert("Timer Closed");
},
error : function(req, status, error)
{
alert("Sorry! We could not receive your feedback at this time.");
}
});
The success and error functions are not necessary. I included them only for demonstration. I also guessed the name of your controller to be Home. You will need to update the URL if it is something different. You could do this with pure javascript. JQuery helps to save you from writing lots of tedious browser compatibility code. (I hear opera is a pain with the unload part)
To address the issue of potential browser crashes and unexpected events you can wire up your controller to check if the timer has exceeded a max time limit.
Good Luck !
Sources :
JQuery Unload()
Invoking ASP.NET MVC Actions from JavaScript using jQuery
JQuery Ajax()
精彩评论