Practical .NET

Saving Data on the Client in ASP.NET MVC

Here's another way to make applications more scalable and more responsive to the user: store some application data on the user's computer.

Vogel's first law of client-server computing is: "The user's computer should be regarded as a free, unlimited resource that should be exploited unmercifully." The obvious justification is that, by using the client's resources, rather than your server's resources, you improve the scalability of your application. And, by reducing trips and payloads between the client and the server, you also create a more responsive application for the user.

Ajax is, of course, one solution that moves data processing to the client. JSX+React (which I've discussed in my TypeScript column even moves generating the page's HTML to the user's computer. The topic of this column, using localStorage, allows you to transfer data storage to the user's computer. To be fair, I feel obliged to point out that there isn't a lot of ASP.NET MVC code in this column -- this is primarily a JavaScript column. However, I will be showing how to pass data from your View to your client in a way that supports saving that data on the user's computer.

Warnings
There are some caveats with using localStorage, the first being that it's part of the HTML5 specification so older browsers don't support it. In addition, browsers impose a limit on the amount of data you're allowed to store in localStorage -- in some browsers, running in "private" mode, that limit is set to zero. To deal with that you'll need to make sure (a) localStorage is available and (b) you haven't hit your limit. In this column, I'll show you how to test for both conditions and to ensure you won't exceed your limit.

There's also some variation between browsers on how localStorage works. In my experience/testing those differences haven't been significant enough to make me swear off using localStorage. At the end of this article, I'll discuss some of those differences.

While I've been referring to localStorage, there are actually two varieties of client-side storage that you can use: sessionStorage (which is, essentially, discarded when the user closes the browser) and localStorage (which survives from one browser session to another). The API for both are similar enough that I'll just discuss using localStorage in this column.

One last note before getting to the code: You need to recognize that localStorage isn't tied to a page -- instead, it's tied to the page's domain (the front part of the page's URL). This gives you two choices when using localStorage: Use it only for data that's common to all applications on the same domain (user preferences leaps to mind) or find a way to keep each application's local data separate from other applications from the same domain. I'll show one strategy for keeping each application's data separate from other applications in the same domain.

Testing for Support
First things first: How do you know you can use localStorage? The following code both checks to see if the browser supports localStorage and that there's space available (there's no direct way to check for how much space you have available). The code first checks to see if localStorage is supported by attempting to retrieve a reference to localStorage. If that works the code then checks to see if there's space available by attempting to store an item in localStorage. If all of that works then the code removes the test item from localStorage. If one of those steps fails (either because the browser doesn't support localStorage or there's no available space) the code will blow up, throwing you into the exception block where you can do whatever seems appropriate:

<script>
try { var storTest = window['localStorage'];
  storTest.setItem("", ".");
  storTest.removeItem(""); }
catch(e) { alert("Please go away"); }

You can, of course, use modernizr to check to see if the user's browser supports localStorage but modernizr won't tell you if there isn't any space available. This code does both.

Speaking of testing: localStorage has a clear function that removes all of the entries in localStorage on the user's computer. I suspect that would be an unusual thing to do in a production application. After all, with the clear function you're not just wiping out your application's local data but all data stored for any application from the same domain. Using clear indiscriminately sounds like a good way to make enemies of developers in other teams. We won't ask how I know this.

However, in order to test your application in development, you'll probably find the clear function helpful in cleaning out localStorage so that you can test how your page behaves when there's no data present. The smart thing to do is put your call to clear on a utility page that you can call when you want to clear out localStorage.

Initializing and Updating the Store
You store data in localStorage in a way similar to how you store data in a dictionary: You invent a key and pass the key and your data to localStorage's setItem method. This code retrieves a reference to localStorage and uses it to add the string "Peter Vogel" under the key "A123":

var ls;
ls = localStorage;
ls.setItem("A123", "Peter Vogel");

With localStorage, you can pass numbers or strings as keys or data but, in the end, everything -- keys and data alike -- is stored as strings.

In order to keep each application's data separate, rather than build up multiple keys for each application, the smarter solution is to create a JSON object that holds all the data for a single application. You can then store that JSON object under a unique name for the application.

You could build up that JSON object in JavaScript code running on the client but, at the very least, you should initialize the application's client-side data with data passed from the server the first time the user accesses your page. If you make that initial object about as large as it will ever get, you can also use this to check that you have enough room in localStorage for your application.

The first thing to do, therefore, is to check if your application's JSON object is already present by attempting to retrieve it from localStorage using localStorage's getItem function. If the key you're using isn't present, getItem won't raise an exception but will, instead, return a null value. When you do get that null value, you can store your initial JSON data in localStorage.

Putting that all together in a View, this code checks to see if there's any data stored under the key "CustomerApplication" and puts it in the variable custStr. If there isn't, the code stores the object from the View's Model property in localStorage by converting it into a string. Once the data is safely stored, the code retrieves the data into the custStr variable. I do that initial setItem in a try block to check if there's enough space to hold my object:

var custStr
custStr = ls.getItem("CustomerApplication");
if (custStr == null)
{
  try {ls.setItem("CustomerApplication", '@Html.Raw(Json.Encode(Model))');}
  catch(e) {alert("Insufficient space");}
  custStr = ls.getItem("CustomerApplication");
}

The single quotes around the second parameter passed to the setItem function are required to avoid conflicts with the double quotes used by the Encode method.

Now that the data has been retrieved from localStorage, you can convert it into a JSON object to use elsewhere in your application:

custJson = JSON.parse(custStr);

If, rather than using a hardcoded string as the data's key, you'd rather use some data from the object, then the code is only slightly more complicated. This code stores the object in the Model property using the value of one of the properties on the object as the key:

ls.setItem("@Model.CustId", '@Html.Raw(Json.Encode(Model))');

Saving the JSON object back to localStorage after it's been changed in the browser requires even less effort. This code updates a value in the JSON object, converts the object back to a string and then updates localStorage with the result:

custJson.Customers[0].LastName = "Irvine";
ls.setItem("CustomerApplication", JSON.stringify(custJson));

You may also want to use this JSON object for other processing. I discussed the techniques for saving JSON, using it in Ajax requests, and posting the object back to the server to be used in an Action method in an earlier column.

If you ever want to get rid of the data, you can use localStorage's removeItem function, passing the key of the item to be removed. This code removes the CustomerApplication entry in localStorage:

localStorage.removeItem("CustomerApplication");

If the key isn't present, removeItem doesn't raise an exception.

Synchronizing Data
As you update items in your localStorage you may want to notify applications from your domain running in other tabs in the browser about your updates. This could, in fact, be critical if you're storing user preference data -- if users are running two applications from your domain in different tabs, those users might reasonably expect changes in their preferences in one tab to show up in the other tabs.

You can support this by using the window object's AddEventHandler function to add an event handler for 'storage.' However, you can't tie the event to a specific key in localStorage -- the event is raised by a change to any item in localStorage. Because other applications might be using localStorage (modernizr uses localStorage, for example), you'll want to check to see what key triggered the event before you respond to it.

The function you write to handle the event is passed a parameter that has two properties that can help you determine whether you want to respond: key and url. The key property contains the key of the item that was changed; the url property contains the full URL for the page in which the code is running.

The parameter passed to your event handler also contains two other properties that hold data related to the change: oldValue and newValue. As you might expect from the names, oldValue has the original data for the key while newValue has the updated value.

This code wires up an event handler that checks to see if the data item that was changed has the key "CustomerApplication." When that's true, the event handler updates the custRollback variable with the new data:

window.addEventListener('storage', function (e) {
  if (e.key == 'CustomerApplication') 
  {
    custRollback = JSON.parse(e.newValue);
  }
});

Handling this event is where the differences among the browsers are concentrated. The major difference is with where the event is raised. The HTML5 specification says that event should only fire in tabs other than the tab where the change was made. In Internet Explorer and older versions of FireFox, the event is also raised in the tab with the code that made the update. Other differences include: Internet Explorer fires the event on every setItem, even if the old and new values are identical; Chrome, on the other hand, fires the event only if the new value is different from the old value; Chrome doesn't raise the event when you call removeItem, Internet Explorer and FireFox do (provided there's something to remove); in the browsers that raise the event on removeItem, newValue is set to a zero length string in Internet Explorer and null in FireFox. This list of differences is not exhaustive: Test carefully if you decide to use the event.

Other than handling the change event, I've found localStorage reasonably predictable. Leveraging localStorage is just another way to make your application more scalable while giving your users a more responsive experience. Nothing wrong with that.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Creating Reactive Applications in .NET

    In modern applications, data is being retrieved in asynchronous, real-time streams, as traditional pull requests where the clients asks for data from the server are becoming a thing of the past.

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

Subscribe on YouTube