Wednesday, January 27, 2010

AJAX and UpdatePanels and SpGridViews and Paging! O MY!!!

The hoops we jump through for usability sometimes. Its crazy!  I mean, how important can it be, that a product is convenient, fast and useful?  Why can't it just be pretty?   /sarcasm

Ok Ive been working on this specific project for over a week now.  One of the requirements was to take two (long) lists of users, and find the difference between the two, then display them in a webpart on a sharepoint site.  Obviously SPGridView comes to mind.

And since I'm a fancy pants, I also wanted to add Paging, a progress bar, and AJAX it! 

I probably should've done it in that order.  But I AJAX'd it, added a progress bar, then finally paging.  I figured that'd be hardest to easiest but turns out it may have been the reverse (paging being the most time consuming).

Now I want to mention some key points to my project, that were in place to get everything working correctly.  Obviously I needed AJAX install on the server.  I downloaded the VNTG.WebConfigModification and enabled that on my farm after adding System.Web.Ajax and AjaxControlToolkit to the GAC.  This let me use the UpdatePanel effectively, as I was having trouble with postbacks even with the ScriptManager and UpdatePanel on the page.  Make sure to have a reference to System.Web.Extensions in your project and to USE System.Web.UI.

Some of these steps *might* be able to be omitted but I am not exactly sure which yet so I will list them all.  If anyone knows which ones are extraneous, please let me know and Ill make note of that.

So lets continue with some code now, shall we?  First make sure you ovveride the OnInit event in your webpart and add your ScriptManager to the page there.  You also want it to be the first control on the page, so your update panels work correctly.  To do that try this:

ScriptManager sm = ScriptManager.GetCurrent(this.Page);
      if (sm == null)
      {
        sm = new ScriptManager();
        sm.EnablePartialRendering = true;
       
      }
      this.Page.Form.Controls.AddAt(0, sm);
      this.EnsureUpdatePanelFixups();
      if (this.spgv == null)
      {
        this.spgv = new SPGridView();
        this.spgv.AutoGenerateColumns = false;
      }
      base.OnInit(e);
I have sm and spgv as private variables in the class.  Ensurepanelfixups() is a routine which strips away an onload script that Sharepoint uses that interferes with UpdatePanel and postbacks.  You can google that and pick one as there are a lot of different versions out there.

Next would be the CreateChildControls() function that should be overridden.
base.CreateChildControls();

        this.up = new UpdatePanel();
        this.up.ID = "upPolicy";
        this.up.UpdateMode = UpdatePanelUpdateMode.Conditional;

        this.upro = new UpdateProgress();
        this.upro.AssociatedUpdatePanelID = this.up.ClientID;
        this.upro.ProgressTemplate = new UpdateProTemplate(@"images/loading.gif", "Loading...");

        this.spgv.PageIndexChanging += new GridViewPageEventHandler(Grid_PageIndexChanging);
        this.spgv.PageSize = 30;
        this.spgv.AllowPaging = true;

        this.up.ContentTemplateContainer.Controls.Add(this.spgv);

        this.Controls.Add(upro);
        this.Controls.Add(up);

        this.spgv.PagerTemplate = null;
       
        if (this.Page.Cache["dt"] != null)
          this.spgv.DataSource = this.Page.Cache["dt"];
        this.spgv.DataBind();

Important things to note:  Make sure you set the PagerTemplate to null AFTER you add the control to your UpdatePanel.  Also, cacheing (and pulling from the cache) the DataTable was the crux of my success in getting the Pager portion working.  The UpdateProgress portion can be taken from here as that tutorial helped me out in getting that working.

One thing that differed a lot from other tutorials was that I was binding my SPGridView on Load (CreateChildControls) as well as on a Button click (because I wanted some user interaction).  This proved to be formidable because I needed to NOT wipe out my DataSet but I needed an empty DataSet when the paged loaded so I didnt get the lovely null reference exception.  Ill post that code so you can easily see whats going on in the button click:
SPSite sps = new SPSite(TARGET_SITE);
        SPWeb spw = sps.OpenWeb();
        SPList spl = spw.Lists[TARGET_LIST];
        List userList = this.GetAllUsersFromList(spl);
        List ADList = this.GetAllUsersEmailFromAD();
        List nonComplianceUsers = ADList.Except(userList).ToList();
        dt = this.MakeDataTable(nonComplianceUsers);

        this.spgv.Columns.Clear();

        SPBoundField bf = new SPBoundField();
        bf.HeaderText = "Email Address";
        bf.DataField = "Email";
        this.spgv.Columns.Add(bf);

        this.spgv.DataSource = dt.DefaultView;
        this.Page.Cache["dt"] = dt.DefaultView;
        this.spgv.DataBind();

 So I make my DataTable in the first block and it gets stored in a Private DataTable: dt.  I clear my columns here so if you click twice, it doesnt duplicate all the columns.  Now I add my column (its just a unique field from our AD structure: Email)  :)   And then I assign the datasource, cache it, and bind.  I'm using constants for my destinations but I may open them up as public properties later such as to be more customizable.  Its not really a part of the requirements so perhaps it will be in V2.0.

Before I wrap up this post there is one more piece of code that is useful to have.  Its the Event handler for the pager.
    private void Grid_PageIndexChanging(object sender, GridViewPageEventArgs e)
    {
      this.spgv.PageIndex = e.NewPageIndex;
      this.spgv.DataBind();
    }
 Easy enough.  And works for my project! The cacheing makes changing pages SUPER fast, really. Try it out, its real fast.   Please let me know if you see any extra code that can be stripped away as I feel I could probably optimize a little better (cant we all?).  Also if youre interested in my ActiveDirectory routine, let me know via comment or email and I could post that as well.

Take care.