I have to admit, this idea all came from the awesome Chinook sample database on CodePlex. It has to be one of the coolest little sample databases I have seen. It has a decent data model, relationship mappings, and plenty of data. What else could a guy ask for, right (well, maybe some graphics)? The coolest thing about the DB is that it is generated from an actual iTunes library (XML file) and can be deployed on multiple database platforms like SQL, MySQL, Oracle, etc.


Follow along, or just steal the code (iTunes.zip) or see it live (iTunes in ASP.NET demo).


Enough about my CodePlex romance, let's build an ASP.NET version of iTunes already! We are going to start with a new ASP.NET 3.5 Web site since I want to use ADO.NET Entity Data Model and ADO.NET Data Services. The first thing I need is the object model. I used the Entity Model wizard and it was able to generate a ready-to-use model in seconds. Gotta love this new fangled code-gen!


itunesmodel


Now that we have a nice model we can build some UI. To break iTunes down into UI parts, we can say that there is a menu of playlists in the left column and an interactive grid of tracks in the right column. Obviously this is a general description of the major elements so forgive me if I am not going into all of iTunes pretty little details.


So let's start with the menu of playlists. We are going to use two semantic Menu's to achieve the proper UI, specifically C1Menu. Here is the markup for the left menu column:




<h3>
Library</h3>
<c1:C1Menu ID="MnuLibrary" runat="server" Orientation="Vertical" Width="100%" AutoPostBack="true">
</c1:C1Menu>
<div class="menublock">
<h3>
Playlists</h3>
<c1:C1Menu ID="MnuPlaylists" runat="server" Orientation="Vertical" Width="100%" AutoPostBack="true">
</c1:C1Menu>
</div>



These two menu's will represent both of the navigation elements in iTunes. The first menu will display the groups included in iTunes to categorize tracks by type. They are Music, Movies, TV Shows, and Audiobooks. The second menu will display playlists specified by the end user in iTunes.

 

Now we need to write a simple query to load our two menus. Notice the first four are added to MnuMain, and the rest are added to MnuPlaylists:


foreach (ChinookModel.Playlist p in pl)
{
p.Track.Load();

if (p.PlaylistId < 5)
{
C1.Web.UI.Controls.C1Menu.C1MenuItem i = new C1.Web.UI.Controls.C1Menu.C1MenuItem();
i.Text = p.Name;
i.Value = p.PlaylistId.ToString();
MnuLibrary.Items.Add(i);
}
else
{
C1.Web.UI.Controls.C1Menu.C1MenuItem i = new C1.Web.UI.Controls.C1Menu.C1MenuItem();
i.Text = p.Name;
i.Value = p.PlaylistId.ToString();
MnuPlaylists.Items.Add(i);
}

}


Now we need an AJAX GridView to display our tracks. I will be using the C1GridView since it has built-in features like sorting, paging, grouping, and more that I can take advantage of.


<c1:C1GridView ID="GrdMedia" runat="server" Width="100%" AutoGenerateColumns="False" AllowPaging="True" AllowSorting="True" PageSize="28" DataSourceID="DataTracks" PagerSettings-Mode="NextPrevious" AllowColMoving="True" AllowGrouping="True" AllowKeyboardNavigation="True" GroupByCaption="Drag track icons into playlists. Double-click tracks to view info. Drag columns here to group grid." GroupIndent="20px" VisualStylePath="~/C1WebControls/VisualStyles">
<PagerSettings Mode="NextPrevious" />
<EmptyDataTemplate>
<div class="empty">
<h2>
Playlist</h2>
<p>
This playlist does not contain any tracks!</p>
</div>
</EmptyDataTemplate>
<Columns>
<c1:C1TemplateField>
<ItemTemplate>
<span class="dragger">
<img src="Images/track.png" alt="Track" />
</span>
</ItemTemplate>
</c1:C1TemplateField>
<c1:C1BoundField DataField="TrackId">
<ItemStyle CssClass="val" />
</c1:C1BoundField>
<c1:C1BoundField DataField="Name" HeaderText="Name">
</c1:C1BoundField>
<c1:C1TemplateField HeaderText="Artist" SortDirection="Ascending">
<ItemTemplate>
<asp:Label ID="LblAlbumArtistName" runat="server" Text=''></asp:Label>
</ItemTemplate>
</c1:C1TemplateField>
<c1:C1TemplateField HeaderText="Album">
<ItemTemplate>
<asp:Label ID="LblAlbumTitle" runat="server" Text=''></asp:Label>
</ItemTemplate>
</c1:C1TemplateField>
<c1:C1BoundField DataField="Composer" HeaderText="Composer" Visible="false">
</c1:C1BoundField>
<c1:C1TemplateField HeaderText="Genre">
<ItemTemplate>
<asp:Label ID="LblGenreName" runat="server" Text=''></asp:Label>
</ItemTemplate>
</c1:C1TemplateField>
<c1:C1BoundField DataField="Milliseconds" HeaderText="Time">
</c1:C1BoundField>
<c1:C1BoundField DataField="Bytes" HeaderText="Size" Visible="false">
</c1:C1BoundField>
<c1:C1BoundField DataField="UnitPrice" DataFormatString="{0:c}" HeaderText="Price">
</c1:C1BoundField>
</Columns>
</c1:C1GridView>



Next we will add a method to load the tracks for a given playlist by PlaylistID. This is a LINQ to Entities query that makes sure to load in all relational objects if they are not already loaded with lazy loading. There is also a method I added that sorts the DataSource using some new features in .NET 4. It's important to note that I included extensions from .NET 4 preview to be able to sort a IQueriable using a sort expression string. If you want to take advantage of this nice feature (to enable sorting in your strongly typed collections) you can get it here, LINQ Dynamic Query Library. The main benefit of using this library is to get sorting features without converting your collections into DataSets or DataTables. I highly recommend giving it a try if you are using 3.5 now.



private ChinookModel.ChinookEntities _c = new ChinookModel.ChinookEntities();

public ChinookModel.ChinookEntities c
{
get { return _c; }
set { _c = value; }
}

public List SelectAllTracksByPlaylistId(int PlaylistId, string sortExpr)
{

var query = (from play in c.Playlist
from track in play.Track
where play.PlaylistId == PlaylistId
orderby track.Album.Artist.Name
select track);



foreach (ChinookModel.Track t in query)
{
if (!t.AlbumReference.IsLoaded)
{
t.AlbumReference.Load();
}
if (!t.Album.ArtistReference.IsLoaded)
{
t.Album.ArtistReference.Load();
}
if (!t.GenreReference.IsLoaded)
{
t.GenreReference.Load();
}
if (!t.MediaTypeReference.IsLoaded)
{
t.MediaTypeReference.Load();
}
}
if (!string.IsNullOrEmpty(sortExpr))
{
query = SelectAllSortTracks(query, sortExpr);
}
return (List)query.ToList();

}

private IQueryable SelectAllSortTracks(IQueryable query, string sortExpr)
{
if (!String.IsNullOrEmpty(sortExpr))
{
query = query.OrderBy(sortExpr);
}
return query;
}

Now that we have the UI and the Query, we need something to bind them. I am going to use one of my favorite ASP.NET controls to do this. The ObjectDataSource control is highly underrated and underused, in my opinion. It is a really powerful data control if you are using strong object models. It makes binding to strongly typed objects a breeze.



<asp:ObjectDataSource ID="DataTracks" runat="server" SortParameterName="sortExpr" SelectMethod="SelectAllTracksByPlaylistId" TypeName="ChinookMedia">
<SelectParameters>
<asp:Parameter DefaultValue="1" Name="PlaylistId" Type="Int32" />
</SelectParameters>
</asp:ObjectDataSource>


 

Now, let's run the project and see what we get.



itunes

 

Looks pretty good, but we haven't really done anything that amazing. yet. So now I want to make a popup Window that displays track information and allows edits to be made to it. To do that I am going to use pure jQuery and an ADO.NET Data Service. That's right, no PostBack necessary! Making a Web service from our model could not be any easier. File > Add New Item > ADO.NET Data Service, add one line of code and you are done. Here is that line of code. WARNING: Do not set this type of access to your Web services unless you want hackers corrupting your database. This is for demo purposes only.


public class Chinook : DataService
{
public static void InitializeService(IDataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
}
}

To make the popup Window I am going to use a C1Window control. Inside it I will add a C1TabControl with two panes, one for display and the other for edits.



<c1:C1Window ID="PopInfo" runat="server" StatusVisible="False" Width="500px" Height="360px" StartPosition="Page" HorizontalAlign="Center" VerticalAlign="Middle" VisualStyle="Vista" ImageHeight="0" AutoExpand="False" ImageWidth="0" AnimationEffectOnShowing="ScrollInFromBottom" AnimationDurationOnShowing="300">
<ContentTemplate>
<c1:C1TabControl ID="TabsInfo" runat="server" Height="280px" UseEmbeddedVisualStyles="True" VisualStyle="Vista" Easing="EaseOutExpo">
<TabPages>
<c1:C1TabPage Text="Summary">
<img src="Images/album.png" alt="Album" />
<hr />
<ul>
<li><strong>Track: </strong>
<asp:Label ID="LblName" runat="server"></asp:Label></li>
<li><strong>Artist: </strong>
<asp:Label ID="LblAlbumArtistName" runat="server"></asp:Label></li>
<li><strong>Album: </strong>
<asp:Label ID="LblAlbumTitle" runat="server"></asp:Label></li>
<li><strong>Time: </strong>
<asp:Label ID="LblTime" runat="server"></asp:Label></li>
<li><strong>Kind: </strong>
<asp:Label ID="LblMediaTypeName" runat="server"></asp:Label></li>
<li><strong>Size: </strong>
<asp:Label ID="LblSize" runat="server"></asp:Label></li>
</ul>
<hr />
</c1:C1TabPage>
<c1:C1TabPage Text="Info">
<div class="formitem">
<asp:Label ID="LblInfoName" runat="server" AssociatedControlID="TxtInfoName" Text="Name"></asp:Label>
<asp:TextBox ID="TxtInfoName" runat="server"></asp:TextBox>
</div>
<div class="formitem">
<asp:Label ID="LblInfoAlbumArtistName" runat="server" AssociatedControlID="TxtInfoAlbumArtistName" Text="Artist"></asp:Label>
<asp:TextBox ID="TxtInfoAlbumArtistName" runat="server"></asp:TextBox>
</div>
<div class="formitem">
<asp:Label ID="LblInfoAlbumTitle" runat="server" AssociatedControlID="TxtInfoAlbumTitle" Text="Album"></asp:Label>
<asp:TextBox ID="TxtInfoAlbumTitle" runat="server"></asp:TextBox>
</div>
<div class="formitem">
<asp:Label ID="LblInfoComposer" runat="server" AssociatedControlID="TxtInfoComposer" Text="Composer"></asp:Label>
<asp:TextBox ID="TxtInfoComposer" runat="server"></asp:TextBox>
</div>
<div class="formitem">
<asp:Label ID="LblInfoGenre" runat="server" AssociatedControlID="CboInfoGenre" Text="Genre"></asp:Label>
<asp:DropDownList ID="CboInfoGenre" runat="server" DataSourceID="DataGenres" DataTextField="Name" DataValueField="GenreId">
</asp:DropDownList>
<asp:ObjectDataSource ID="DataGenres" runat="server" SelectMethod="SelectAllGenres" TypeName="ChinookMedia"></asp:ObjectDataSource>
</div>
</c1:C1TabPage>
</TabPages>
</c1:C1TabControl>
<div class="trackinfo">
</div>
</ContentTemplate>
<CaptionButtons>
<CollapseExpandButton Visible="False" />
<CloseButton Visible="True" />
<Icon Visible="False" />
<MaximizeButton Visible="False" />
<MinimizeButton Visible="False" />
<PinButton Visible="False" />
<ReloadButton Visible="False" />
</CaptionButtons>
</c1:C1Window>


Now for the fun part, we are going to use jQuery to call the ADO.NET Data Service to get data for a single track. To do this we will attach a double-click event to each row in the GridView. When the event fires we will call the service and load in the data to the proper fields and pop up the Window to display them.


$("#GrdMedia_datatable tbody .C1Row").dblclick(function() {
getTrack($(this).find(".val").text());
return false;
});
function getTrack(trackId) {
$(".trackinfo").html("");

$.ajax({
type: "GET",
url: "Chinook.svc/Track(" trackId ")?$expand=Genre,MediaType,Album,Album/Artist",
data: "{}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(data) {
displayTrack(data.d);
},
error: function(xhr) {
alert(xhr.responseText);
}
});

return false;
}
function displayTrack(track) {

$("#").text(track.Name);
$("#").text(track.Album.Artist.Name);
$("#").text(track.Album.Title);
$("#").text(track.Milliseconds);
$("#").text(track.Bytes);
$("#").text(track.MediaType.Name);

$("#").val(track.Name);
$("#").val(track.Album.Artist.Name);
$("#").val(track.Album.Title);
$("#").val(track.Composer);
$("#").val(track.Genre.GenreId);
var pop = $find("PopInfo");
pop.set_text(track.Name);
pop.show();
}


OK, that was easy, but will it work? Woot!



itunespopup




I haven't included the code here to update the data when it is edited, but you get the picture. Instead of doing that, I am going to show you how to utilize jQuery even further to add drag-and-drop capabilities to a GridView! Don't get too excited, please.

 

In order to implement drag-and-drop, I will need to reference jQuery UI in my ScriptManager. Notice, I do not need a reference to jQuery since all of the ComponentOne ASP.NET controls already do that for me. I downloaded a custom version of jQueryUI since all i need is the draggable and droppable plugins. There is alot of code in jQueryUI and you should only be including what you will use (by all means include all of it as long is it doesn't go wasted).

 

Since I do not want the end user dragging an entire row of a GridView, I am going to add a template column with an icon in it. The icon is actually what will be draggable. I also want to make a custom template so when the item is being dragged it shows a larger album icon. This is done when initializing the draggable plugin.


$("#GrdMedia_datatable tbody .C1Row .dragger").draggable({
cursor: 'move',
cursorAt: { top: 64, left: 64 },
helper: function(event) {
return $('
Album
'
);
}
});
$("#MnuPlaylists .C1MenuItem").droppable({
hoverClass: 'C1MenuItem C1Hover',
drop: function(event, ui) {
alert("Added to playlist: " $(this).text());
}
});


There you have it! An awesome ASP.NET app that mixes classic server-side dev with some cool new client-side tricks. I really enjoyed making this app and my experience working with jQuery and ADO.NET Data Services has been awesome. I can't wait for the next volume of Popular UI so I can use it some more!

 

p.s. Props to "Jeb" for the awesome CSS styling. Strong work my UI Ninja friend.