Problem

The accordion control provided by the Ajax Control Toolkit is a great control to display expandable items in a list. Depending on the amount of data you want to display when an item is expanded, the page that contains the control can quickly become huge because you have to load in the page all the data shown in the expanded version of all the items.

Solution

Here comes the Load On Demand accordion control !

Instead of loading all the data up front, it fetches the data of the selected item only using Ajax.

imageimage

Overview

On your page, you will have two invisible panels, one that contains the html code you want to display whenever you are loading an item, and a second that is wrapped in an UpdatePanel control, that will retrieve the html code of the selected item. When the ajax request of the UpdatePanel finishes, you simply copy the content into the content panel of the selected item.

On the asp.net side

The code I use for the invisible loading panel:

<div id=”loadingPanel” class=”invisible”>
    <img src=’ajax-loader.gif’ /> Loading…
</div>

The code I use for the update panel:

<asp:UpdatePanel ID=”updatePanel” runat=”server”>
    <ContentTemplate>
        <p>Invisible update panel:
        <div class=”invisible”>
            <asp:Panel ID=”panelUpdated” runat=”server”>
               <uc1:DetailedPlayerView ID=”detailedPlayerView” runat=”server” />
            </asp:Panel>
            <asp:HiddenField ID=”hiddenPlayerId” runat=”server”/>
            <asp:Button ID=”btnUpdate” OnClick=”btnUpdate_PlayerView” runat=”server” Text=”Button” class=”invisible”/>
        </div>
        </p>
    </ContentTemplate>
</asp:UpdatePanel>

When a user clicks on the header of an item, a javascript function is called, performing the following actions:

– Save the selected panel in a javascript variable to refer to it when the callback function is called

– Store the selected data item id in an hidden field (to be able to display data specific to that id)

– Copy the content of the loading panel into the content panel of the selected item

– Click the button btnUpdate to update the UpdatePanel.

The javascript code looks like this:

var selectedContentPanel = “”;

function fetchContent(contentPanel, playerID)
{
    // save selected panel
    selectedContentPanel = contentPanel;
    // save playerId value in hidden field
    $get(“<%= hiddenPlayerId.ClientID%>”).value = playerID;
    // show the loading picture
    $get(contentPanel).innerHTML = $get(“loadingPanel”).innerHTML;
    // call postback
    $get(“<%= btnUpdate.ClientID%>”).click();
}

You need to subscribe to the endRequest events to be able to update the content panel based on the UpdatePanel content:

Sys.WebForms.PageRequestManager.getInstance().add_endRequest(EndRequestHandler);

function EndRequestHandler(sender,args)
{
    // update selected panel with postback result
   $get(selectedContentPanel).innerHTML = $get(“<%= panelUpdated.ClientID %>”).innerHTML;
}

On the C# side

You can update the content of the UpdatePanel in the event handler of the Click event of the invisible button in the UpdatePanel

protected void btnUpdate_PlayerView(object sender, EventArgs e)
{
    var playerId = long.Parse(hiddenPlayerId.Value);
    var player = _players.Find(x => x.PlayerId == playerId);
    detailedPlayerView.Player = player;
}

Calling fetchContent

To be able to call the javascript fetchContent method when the header is clicked, you need to add a onClick attribute. Because I want to be able to pass the id of the content panel, I found it easier to add the onClick attribute on the C# side.

I subscribed to the ItemDataBound event of the Accordion control and handled it this way:

protected void accordionPlayers_ItemDataBound(object sender, AccordionItemEventArgs e)
{
    if (e.AccordionItem.ItemType == AccordionItemType.Content)
    {
        var player = (PlayerInfo)e.Item;
        var contentPanel = (Panel)e.AccordionItem.FindControl(“panelContent”);
        var headerPanel = (Panel)e.AccordionItem.Parent.Controls[0].FindControl(“panelHeader”);
        // add to the header panel an onclick attribute that passes the id of the player and the content panel
        var fetchContent = string.Format(“fetchContent(\”{0}\”, {1});”, contentPanel.ClientID, player.PlayerId);
        headerPanel.Attributes.Add(“onclick”, fetchContent);
    }
}

I hope you will find this blog post useful, don’t hesitate to leave a comment!

Cheers

Advertisements

Yesterday, I spent most of my day studying the MVC Framework Preview 3. To drive my research, I decided to try to redevelop a web site I previously wrote in ASP.NET WebForms. Because my web application is similar to the sample project of ScottGu (with products and categories), for the sake of brevity, I will keep his sample.

The intent of this blog post is to help new comers to have a quicker start, by exposing the difficulties I overcame yesterday.

Download link

First you need to install MVC Framework: http://go.microsoft.com/?LinkID=8955108

Using Html.ActionLink()

The Html class provides several handy helper functions that are a (very) light replacement to the Asp.Net controls.

One of them, ActionLink() builds a hyperlink, based on the given parameters.
In the Global.asax file, I have the following Route defined:

image

To be able to get a edit link on a product that points to http://localhost:56181/Products/Edit/5, we can useimage

Remark: in the third argument of ActionLink(), the name of the properties of the anonymous type should match the names used in the route (defined in the Global.asax).

For example, if I’d write:image

I would get the following link: http://localhost:56181/Products/Edit?productId=5

Cannot access a disposed object

I am using Linq to Sql to communicate with the database. To implement the List() action for the categories, I first wrote something like:

public ActionResult List()
{
    using (var db = new ProductsDataContext())
    {
        List<Category> list = db.Categories.ToList();
        return View(list);
    }
}

This works fine… until you call category.Products in the view. You would get the too famous Linq to Sql error “Cannot access a disposed object” because Lin q to Sql is trying to access the data context to fetch from the database the products of the category.

I first thought the view would be resolved during the “return View(list)”, before the data context is resolved. It seems it is done outside the List() method. So the easy workaround I found for this specific case was to use DataLoadOptions to load the related products of the category at the same time.

public ActionResult List()
{
    List<Category> list;
    using (var db = new ProductsDataContext())
    {
        var dlo = new DataLoadOptions();
        dlo.LoadWith<Category>(x => x.Products);
        db.LoadOptions = dlo;

        list = db.Categories.ToList();
    }
    return View(list);
}

(related external link)

How to submit a form?

To create a “edit product” page, like ScottGu’s example, we can use the Html class and the ViewData property:image

Remark: although the view Edit.aspx is called by giving a product id in the url, like “/Products/Edit/5”, I didn’t find an easy way to retrieve the parameter value from the view to use it in the form. the workaround is to put the product id in the ViewData in the controller class.

As we can see in the last screenshot, on submit the form will call the Save action. The Save action doesn’t require a view, we can save the data in the database from the controller and redirect to another action calling RedirectToAction:

return RedirectToAction("List", "Products");

Remark: I got some blank pages without explicit errors because I forgot to give a return parameter to the action methods in my controller. Do not forget to return a ActionResult in your action methods (RedirectToAction returns a RedirectToRouteResult object, this is a valid ActionResult to return)

Passing parameters to View UserControls

First, the following page is an interesting article about MVC compatible UserControls: link

To be able to pass more complex objects than strings to a view user controls, I used the Html.RenderUserControl() function, passing the data in the second argument:

image

Conclusion: when can I use MVC Framework?

Pros

  • Extensibility
  • Unit test friendly
  • Better control over produced html
  • SEO friendly (friendly urls for example)

Cons

(find more with question asked in forum)

At the moment, the MVC framework would be a risky choice for most of the projects I work on.

But because of the excellent extensibility of the MVC Framework, we can expect that the number of cons will be reduced over time thanks to the contributions of the community.

Today I had to undo the pending changes of my user from a different workspace and I used the following command (from a Visual Studio command prompt on the TFS server):

tf undo /workspace:workspacename /server:http://urltotfs:port /recursive “$/projectname”

Summary

I have written a Visual Studio macro that turns Visual Studio into a Snippet Editor. You simply select the piece of text you want to use in your snippet and the macro will create a snippet that will automatically be loaded by Visual Studio.

Download the latest macro here.

How to use it?

Write the snippet code from within Visual Studio as if you were writing code. Use the syntax $Anything$ to declare Intellisense literals. ($end$ and $selected$ are already used by Visual Studio)

Run the macro from the Macro Explorer:

I suggest you to associate it to a keyboard shortcut, like Alt+S:

The macro will prompt you for the name of the new snippet:

That’s it! You can now use your new snippet in Visual Studio.

How to install it?

Open the Macro Explorer by pressing Alt+F8 or through the Tools menu:

In the Macro Explorer, right-click on the Macros node, select “Load Macro Project…” and choose the vsmacros file you downloaded (here)

You should be able to see the “CreateSnippet” macro in the Macro Explorer:

Finally, you will need to edit the macro to fit your environment settings:

Change the author and snippetsPath variables. snippetPaths should be a path that has been added to the Macros Manager (the My Code Snippets folder exists by default)

Save the macro and you are good to go!

Thanks

Big thanks to Patrick Galluci who wrote this article about snippets. My macro is a simple improvement of his great macro.

The fact that sql2005 can host the CLR was a big selling point. I kind of always knew that it was possible to extend and create new SQL functions in C# but never really took the time to do it myself.

What do we want to do?

I’d like to be able to create a function that works like the .NET String.Join static method. Here is a sample query that would demonstrate the use our new aggregate function:

select PizzaRecipeId, dbo.strjoin(IngredientName)
from PizzaRecipe
group by PizzaRecipeId

This query would output something like:

1 Cheese, Chicken, BBQ Sauce
2 Cheese, Ham
3 Cheese, Chicken, Pineapple

Let’s do it 🙂

The first thing is to write the c# code.
Create a new class library project and add a new class called Concatenate. Our new class must:
– have a serializable attribute
– have the SqlUserDefinedAggregate attribute
– implement IBinarySerialize
– have the following 4 methods (there is no interface for them)
o public void Init()
o public void Accumulate(SqlString value)
o public void Merge(Concatenate other)
o public SqlString Terminate()

The implementation itself is straightforward, using a string builder to store the data:

[Serializable]
[SqlUserDefinedAggregate(Format.UserDefined, MaxByteSize=8000)]
public class Concatenate : IBinarySerialize
{
private StringBuilder sb;

public void Init()
{
sb = new StringBuilder();
}

public void Accumulate(SqlString value)
{
if (sb.Length != 0)
{
sb.Append(“, “);
}

sb.Append(value);
}

public void Merge(Concatenate other)
{
sb.Append(other.sb);
}

public SqlString Terminate()
{
return new SqlString(sb.ToString());
}

#region IBinarySerialize Members

public void Read(BinaryReader r)
{
sb = new StringBuilder(r.ReadString());
}

public void Write(BinaryWriter w)
{
w.Write(sb.ToString());
}

#endregion
}

If the project compiles, let’s continue with the commands that will actually load the dll into sql server and reference the aggregate function:

create assembly clr_integration from ‘C:\dev\learning\sql2005\ \clr_integration\bin\debug\clr_integration.dll’ with permission_set = safe
GO

create aggregate strjoin(@input nvarchar(200)) returns nvarchar(max)
external name clr_integration.Concatenate

The permission_set is safe because the assembly does not perform any interop or contains any unsafe blocks.

Let’s create a table and some dummy data:

if (object_id(‘PizzaRecipe’) is not null) drop table PizzaRecipe
create table PizzaRecipe
(
PizzaRecipeId int NOT NULL,
IngredientName nvarchar(200)
)

insert PizzaRecipe values(1, ‘Cheese’)
insert PizzaRecipe values(1, ‘Chicken’)
insert PizzaRecipe values(1, ‘BBQ Sauce’)
insert PizzaRecipe values(2, ‘Cheese’)
insert PizzaRecipe values(2, ‘Ham’)
insert PizzaRecipe values(3, ‘Cheese’)
insert PizzaRecipe values(3, ‘Chicken’)
insert PizzaRecipe values(3, ‘Pinnaple’)

Everything should now be ready and it’s time to test it 🙂

Writing the aggregate function was actually simpler than what I imagined. It’s now just a matter of detecting when this type of feature can be used.

TFS: removing _svn folders

October 11, 2007

I recently had the need to clean up a project in TFS. When it was imported, little care had been put in making sure that only the required files would be on source control.

The most noticable folders that were to be removed from TFS were: ‘_svn’, ‘bin’ and ‘obj’.

As I’m not a big fan of doing things manually (yes, even if doing it manualy is faster – well in that case it was probably faster to write the script) I ended up trying to remember how to do those fancy FOR loops in DOS 🙂

First, build a list all the folders (or files) you want to remove:

dir /s /b _svn bin obj > files_to_delete

Creating that file is handy and you can edit it before running the following query:

for /f “delims=” %g in (files_to_delete.txt) do tf.exe delete “%g” /login:DOMAIN\login,password

note: make sure that tf.exe is in your path, or replace it with the full path.

you can now refresh your pending checkin in visual studio and check it in !

I have read quite a few times that exceptions were slow. I came to wonder about how slow they actually were? Is that big enough to care about it ? After all, hardware is pretty fast nowdays, isn’t it?

Well it turns out that throwing and catching an exception takes about 6ms on my machine (Core Duo, 2ghz)

I you want to know why it takes 6ms, have a look at this fantastic post 🙂

Now, a few milliseconds, it does not look like a long time, but think about a web server, handling hundreds of requests at the same time. If each request uses a few Int.Parse that fails(or whatever code that may throw exceptions) , that could end up being quite significant.

This is a simplified version of the code I used (removed the loop to get an average value):

private void Execute()
{
    Stopwatch sw = new Stopwatch();
    sw.Start();

    ThrowAndCatchException();

    sw.Stop();

    Console.WriteLine(“Time taken: {0}ms”, sw.ElapsedMilliseconds);
}

private void ThrowAndCatchException()
{
    try
    {
        throw new Exception();
    }
    catch (Exception) { }
    {
    }
}