2

I currently have a WinForms app that I'm converting over to ASP and am quickly realizing the importance of caching data.

In my WinForms app, I created the form-level variable Dictionary(of String, List(of String)) which I populated in my Form_Load() event using data I pulled from a SQL Server DB to associate 2 linked combo-boxes together (and that was used throughout my program for various other things). The beauty of the WinForms app is that this variable was useable for me throughout the program due to it being a form-level variable and it dying along with the form.

Now, I'm trying to get the same kind of functionality in my ASP project, so I set it as a property and associated it with the cache as follows:

Private ReadOnly Property MyDict As Dictionary(Of String, List(Of String))
    Get
        Dim dict As Dictionary(Of String, List(Of String))
        dict = Cache("MyDict")

        If dict Is Nothing Then

            ... QUERY DB AND POPULATE DICTIONARY ...

            Cache("MyDict") = dict
        End If

        Return dict
    End Get
End Property

My first (and most important) question is whether I am doing this correctly AT ALL or do I just not understand caching well enough - truly, this is my first stab at ASP and it's proving rather intimidating.

My next question is, if I am doing this right, how / where do I declare the life-time of the cache? I noticed that when I re-ran my program, the cached data was still available from previous runs... I'd prefer it be available for the lifetime of the page, but not once it's closed (if that's possible.

Thirdly, ANY good advice / links / tricks would be GREATLY appreciated!!

Thanks!!!

PS - I'm aware that my code is VB, but C# answers are fine too since this is more .Net concept-level rather than language-specific. Thanks.

John Bustos
  • 19,036
  • 17
  • 89
  • 151

2 Answers2

1

The cache approach is a bit problematic, and probably overly complicated for what you're trying to achieve. The questions you have about setting the lifetime of the cache are some of the symptoms of why this is a non-optimal approach. Using a more traditional approach to setting dependent drop-downs saves you a lot of the hassle.

One of the hardest things to get used to in ASP.NET when coming from WinForms development is exactly what you're struggling with - the fact that server-side data is "lost" between posts.

In a WinForms application,you can declare a DataTable, for example, as a global variable, and something as simple as clicking a button or changing the value of a drop-down list doesn't affect the fact that that DataTable is still there, populated. It's there because the table "lives" until the form is disposed. With ASP.NET, server-side objects only exist during the postback while the page is processing, and need to be re-created at post-back.

Most of the magic behind abstracting this is done via ViewState, which saves the state of the objects on your page between post-backs. For example, if you were to, in code, query a database, save the results to a DataTable, and then bind that DataTable to a DataGrid, the ViewState would save the contents of the DataGrid. On the next postback, the contents of the DataGrid are available, but the initial DataTable that was bound to the DataGrid no longer exists.

Trying to use the cache to imitate the persistent lifetime of an object to make an ASP.NET application act like a WinForms application is a misuse of the technology. it's not inherently bad, and it's not impossible to do so, it's just using the wrong approach. It's basically using the wrong tool for the job.

Rather than trying to do this, you'd be better served searching for examples of how to perform the specific task. In your case, you'd want to search for "Cascading Drop-Down List ASP.NET" or something along those lines. Several options are available, but this one is as good as any: http://www.aspsnippets.com/Articles/Creating-Cascading-DropDownLists-in-ASP.Net.aspx

Also, if you're not already familiar with it, it's very important that you understand the ASP.NET Page Lifecycle when writing a .NET app. There is no single component of ASP.NET development that throws new ASP.NET developers off more than a failure to understand the Page Lifecycle. Understanding this will help you more than anything else we can throw at you, and it has a direct relevance to the question you're asking.

David
  • 72,686
  • 18
  • 132
  • 173
  • Thank you so much for all the info, @David - The ViewState in particular is definitely worthwhile knowing for this situation!! The LifeCycle is still going to need another read (or 5), but now at least I have some good information to consider. Thank you!!!! – John Bustos Feb 26 '13 at 17:36
  • 1
    @JohnBustos If you do go down the route of using the ViewState and you are saving a lot of data in the ViewState, you might want to set `maxPageStateFieldLength` in web.config to something like `"4000"` Refs: http://stackoverflow.com/questions/2585154/what-is-an-optimal-value-for-maxpagestatefieldlength-in-asp-net and http://msdn.microsoft.com/en-us/library/950xf363%28v=vs.100%29.aspx – Andrew Morton Feb 26 '13 at 19:12
1

First of all, in Windows Forms you have 1 process inside of which you hold 1 instance of the main form (I will not discuss secondary forms, or the case which is totally acceptable where you create a multitude of instances of the main form).

Here, in ASP.NET you have 1 process inside of which there exist a number of "main" page instances. One might say there is one for each distinct user accessing your web application. That is partially correct: The instantaneous number of "main" page instances can be greater than that of the active users (we are talking about ASP.NET which is not MVC).

You can still access things globally, just like you used to do in WinForms. The only differences are:

  1. In WinForms because in general the main form was unique you could use it as a global container for stuff. In ASP.NET's case you can't do that because there isn't just ONE global instance of the main page (for instance, even in the case of the same session, coming from the same browser, refreshing the page will most likely create a new Page instance. To test that: implement the otherwise implicit public parameterless constructor of that page and use a breakpoint to check that out)

  2. It is generally dangerous (especially if you don't know what you're doing very well) to make all the different threads, which are treating requests coming from many browsers, all access some unique shared memory.

I personally do not entirely agree with the following idea, but it is generally the case that a beginner should simply bombard the database with fairly redundant SELECT commands.

I'll simplify your problem a bit just to be able to give a simple solution that doesn't bombard the database: Say you didn't need a cache. You needed just to read a bunch of stuff from the database and agree with the fact that you'll never read it again until you restart the web application. Once read the information should be available in all corners of the web application.

If that were the case, then there would be an easy solution:

  1. Use the wellknown "Global Application Class" ( Global.asax ) and its "Application_Start" method in order to be notified when your application starts (just add it to your project as you would any source file, you'll find it in the Add New Item dialog)

  2. Use the global HttpApplicationState class which works like a Dictionary and enables the sharing of global information within your ASP.NET application

like so:

public class Global : System.Web.HttpApplication {

    protected void Application_Start(object sender, EventArgs e) {
        // .. read the database here

        HttpContext.Current.Application["SOME_KEY"] = "ANY OBJECT";
        // .. etc
    }

this way you'll be able to read what you've written in the global HttpApplicationState instance from anywhere in your ASP.NET application like so:

public partial class WebForm2 : System.Web.UI.Page {
    protected void Page_Load(object sender, EventArgs e) {

        object obj = this.Context.Application["SOME_KEY"];
        // ...etc...
    }
}

About re-running your app: Most of the time, the web server (especially IIS, but also ASP.NET Development Server) doesn't stop. It starts whenever you want to "re-run" your app if it was stopped. But when you stop debugging (click the Stop button in Visual Studio) that's all you're doing. You detach from the web server's process and leave it running in peace.

When you "re-run" the app, if the web server was already running (sometime ASP.NET Development Server crashes, IIS crashes too but not that often) you are just "re-attaching your IDE's debugger" to the web server, and you discover everything is the same.

There's more to that: If you rebuild (by force, in case a rebuild was not necessary) the web server doesn't stop, but it does discard your app (which runs in an isolated AppDomain) and reloads the new assemblies and starts it again. You can see all these things if you log the Global.asax "Application_Started" method.

EDIT

Here's a safe way to have your cache (that although is optimized to allow many readers have concurrent access to some global data, will still slow things down a bit -- it's a luxury to have a cache, what can I say :)).

First write a class like this one:

public sealed class SafeCache<T> {

    private readonly ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
    private readonly Func<T> expensiveReader;

    private readonly TimeSpan lease;
    private DateTime lastRead;
    private T data;

    public SafeCache(TimeSpan lease, Func<T> expensiveReader) {
        this.lease = lease;
        this.expensiveReader = expensiveReader;
        this.data = expensiveReader();
        this.lastRead = DateTime.UtcNow;
    }

    public T Data {
        get {
            this.rwLock.EnterReadLock();
            try {

                if (DateTime.UtcNow - this.lastRead < this.lease)
                    return this.data;

            } finally {
                this.rwLock.ExitReadLock();
            }

            this.rwLock.EnterUpgradeableReadLock();
            try {

                if (DateTime.UtcNow - this.lastRead < this.lease)
                    return this.data;
                else {
                    this.rwLock.EnterWriteLock();
                    try {

                        this.data = expensiveReader();
                        this.lastRead = DateTime.UtcNow;

                        return this.data;

                    } finally {
                        this.rwLock.ExitWriteLock();
                    }
                }

            } finally {
                this.rwLock.ExitUpgradeableReadLock();
            }
        }
    }

}

Then use Global.asax to create an instance of it and place it in the HttpApplicationState global instance, at some arbitrary key:

public class Global : System.Web.HttpApplication {

    protected void Application_Start(object sender, EventArgs e) {
        HttpContext.Current.Application["SOME_KEY"] = new SafeCache<SomeRecord[]> (
          lease: TimeSpan.FromMinutes(10),
          expensiveReader: () => {
             // .. read the database here
             // and return a SomeRecord[]
             // (this code will be executed for the first time by the ctor of SafeCache
             // and later on, with every invocation of the .Data property getter that discovers 
             // that 10 minutes have passed since the last refresh)
          }
        );
        // .. etc
    }

You could also create a little Helper like:

public static class Helper {
    public static SomeRecord[] SomeRecords {
        get { 
           var currentContext = HttpContext.Current;
           if (null == currentContext) // return null or throw some clear Exception

           var cache = currentContext.Application["SOME_KEY"] as SafeCache<SomeRecord[]>;
           return cache.Data;
        }
    }
}

And of course, use that accordingly wherever you need it:

public partial class WebForm2 : System.Web.UI.Page {
    protected void Page_Load(object sender, EventArgs e) {

        SomeRecord[] records = Helper.SomeRecords;
        // ...etc...
    }
}

END OF EDIT

Eduard Dumitru
  • 3,242
  • 17
  • 31
  • I truly can't thank you enough for such a thorough and detailed answer, @Eduard Dumitru - Truly AMAZING!! Let me ask you, as a seasoned ASP developer, what would you do? Would you go the route of the Cache / Application_Start method as you mentioned or would is it better to program the form to re-query the DB for specific data elements based upon user selections?? Nonetheless, the Cache you mention is truly an AMAZING bit of code **I WILL use**, but I'm more curious what is better practice overall - Not given my trying to emulate WinForms? – John Bustos Feb 26 '13 at 16:48
  • 1
    I am blushing :) (thanks for the flowers, and I think I am mediocre in ASP.NET, believe me :)). I think you should ask yourself the question: Would it be alright for Facebook if each of its users had the possibility to trigger some (synchronized and well controlled) process which changes an important global state which everyone else will notice / be affected by ? (the answer is no, 1st of all there's way too many people and Facebook has only so much computational power)But is your app like Facebook?Or is it for home use,and deployed on a PC,and every family member can: refresh the weather info – Eduard Dumitru Feb 26 '13 at 17:15
  • And just to wrap it up:Everyone is supposed to be doing what they like best(if they can survive financially in a decent way of course).I'm sure that most of the currently or ex WinForms, DelphiVCL or even native VB+ActiveXs developers would agree that: The web is a place upside down.So it seems at least when you enter it coming from that direction.It's actually alright,but just harder to swallow.Many are just affected by the lack of WYSIWYG UI builders.I'm not part of VWG team,and I'm not saying _no to ASP_,but have a look at:[VisualWebGUI](http://www.visualwebgui.com/tabid/570/Default.aspx) – Eduard Dumitru Feb 26 '13 at 17:22
  • ... Darn, so you know I work for FaceBook, huh?? :p Thanks for the analogy!! – John Bustos Feb 26 '13 at 17:22
  • I wasn't trying to make your app look small btw comparing it with Facebook/Home web site.I hadn't enough characters to say this but, in our company for instance, there are small departments (for instace DBAdmins) where they have small web applications in which what you asked is totally possible and ok.They're only 10 people, they know each other very well, and one of them does a _refresh of all DB tables and their columns_ which brings that information into a cheaper to access place (your cache). And in that case,which is no joke, it's ok they can do what you asked.It depends.You decide – Eduard Dumitru Feb 26 '13 at 17:28
  • Truly, man, I am in no way upset by your analogy and feel nothing but gratitude for your responses and help! - Thank you!! – John Bustos Feb 26 '13 at 17:33