When you're dealing with hierarchical data (a tree-like data format such as folders that have sub-folders that have subfolders, etc.), you have to come up with an organized way of displaying it. You could create a tree view, similar to how Windows Explorer displays folders, but I wanted to try something different in Windows 8 - cascading ListBox
es. The user selects an item in a ListBox
, and then a new ListBox
appears to the right showing the sub-categories. This continues as long as there's data left to drill into.
For a data source, I'll use the eBay category list API that I blogged about previously.
Begin by creating a Windows Store project - a Blank App. Add a Basic Page called ChooseCategory.xaml, and delete the MainPage.xaml file that was created with the app. (For this demo, you could get away with the blank page, but it's good to have the Grid
and the VisualStateGroup
automatically added) Open up App.xaml.cs and change this line:
if (!rootFrame.Navigate(typeof(MainPage), args.Arguments))
to call ChooseCategory
instead of MainPage
. Finally, in App.xaml, change the Theme to Light by adding RequestedTheme="Light"
to the Application tag. You could leave it dark by default, but then your ListBox
es will look a bit out of place unless you modify their style.
Next, I'll add a class file to the project to hold a List
that can be bound to each ListBox
, as well as hold the method to make the call to the eBay API. This is almost identical to the previous post, with a few minor tweaks.
Here are the two classes to hold the List
:
public class Categories
{
private List<CategoryDetails> _Items = new List<CategoryDetails>();
public List<CategoryDetails> Items
{
get
{
return this._Items;
}
set
{
this._Items = value;
}
}
}
public class CategoryDetails
{
public string categoryName { get; set; }
public int categoryId { get; set; }
}
You could have more data in these if you wanted, but since this is a ListBox
demo, I just wanted some simple name/value pairs.
And here is the method that makes the call to the eBay API:
public async Task<Categories> CallGetCategoryInfo(int CategoryID)
{
HttpClient httpClient = new HttpClient();
string api_key = "MyKey";
string searchUrl = "http://open.api.ebay.com/Shopping?callname=GetCategoryInfo";
string requestUrl = searchUrl + "&appid=" + api_key + "&version=679&siteid=0&CategoryID=" +
CategoryID.ToString() + "&IncludeSelector=ChildCategories";
HttpResponseMessage response = await httpClient.GetAsync(requestUrl);
System.Xml.Linq.XDocument doc = System.Xml.Linq.XDocument.Load
(await response.Content.ReadAsStreamAsync());
XNamespace ns = "urn:ebay:apis:eBLBaseComponents";
var query = from categories in doc.Descendants(ns + "Category")
select new
{
CategoryID = categories.Element(ns + "CategoryID").Value,
CategoryParentID = categories.Element(ns + "CategoryParentID").Value,
CategoryName = categories.Element(ns + "CategoryName").Value,
LeafCategory = categories.Element(ns + "LeafCategory").Value,
};
List<CategoryDetails> catList = new List<CategoryDetails>();
Categories c = new Categories();
foreach (var element in query)
{
if (element.CategoryID != "-1" && element.CategoryID != CategoryID.ToString())
{
CategoryDetails cd = new CategoryDetails();
cd.categoryId = Convert.ToInt32(element.CategoryID);
cd.categoryName = (Convert.ToBoolean(element.LeafCategory) ?
element.CategoryName : element.CategoryName + " ->");
catList.Add(cd);
}
}
c.Items = catList;
return c;
}
Now let's modify ChooseCategory.xaml. Change the AppName
:
<x:String x:Key="AppName">Choose Category</x:String>
And add a StackPanel
right below the grid that hold the pageTitle TextBlock
:
<StackPanel x:Name="pnlMain" HorizontalAlignment="Left"
Margin="20,10" Grid.Row="2"
VerticalAlignment="Top" Orientation="Horizontal"/>
This is the panel that we'll add the ListBox
es to.
Now for the code-behind in ChooseCategory.xaml.cs. Declare a few variables in the class:
private int _categoryID = -1;
private string _categoryName = "";
private List<CategoryDetails> _cbp = null;
private int _listBoxCount = 0;
Add a method to retrieve the data from eBay and call the method that adds a new ListBox
:
private async void CallGetCategoryInfo(int CategoryID)
{
CategoriesDataSource cds = new CategoriesDataSource();
Categories c = await cds.CallGetCategoryInfo(CategoryID);
_cbp = c.Items;
AddListBox();
}
Now, we add a new ListBox
if we had some data returned from eBay. We set the DisplayMemberPath
and SelectedValuePath
to the public
properties of the CategoryDetails
object, give the ListBox
a distinct name (we'll need it later), wire up an event handler, and add the ListBox
to the StackPanel
:
private void AddListBox()
{
if (_cbp.Count > 0)
{
_listBoxCount += 1;
ListBox listBox1 = new ListBox();
listBox1.DisplayMemberPath = "categoryName";
listBox1.SelectedValuePath = "categoryId";
listBox1.ItemsSource = _cbp;
listBox1.Width = 250;
listBox1.Name = "CategoryLevel" + _listBoxCount.ToString();
listBox1.SelectionChanged += ComboBox_SelectionChanged;
pnlMain.Children.Add(listBox1);
}
}
Finally, we're going to handle selecting an item in the ListBox
:
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Selector list = sender as Selector;
int clickedCategoryLevel = Convert.ToInt32(list.Name.Replace("CategoryLevel", ""));
for (int x = _listBoxCount; x > clickedCategoryLevel; x--)
{
pnlMain.Children.RemoveAt(x - 1);
}
_listBoxCount = clickedCategoryLevel;
_categoryID = Convert.ToInt32(list.SelectedValue);
CallGetCategoryInfo(_categoryID);
}
If the user backs up a level (i.e., they have drilled down a few levels, but now want to click something at a higher level and start over instead of drilling down further), then we need to delete any ListBox
es to the right of the one that was just clicked. Then, we call CallGetCategoryInfo
again to retrieve the data and add a new ListBox
.
Finally, add a call to CallGetCategoryInfo
in the constructor to start the whole process off:
public ChooseCategory()
{
this.InitializeComponent();
CallGetCategoryInfo(_categoryID);
}
Fire up the app and try drilling down and back up. If everything works correctly, you should get results looking similar to this:
Notice that some of the lines end with "->
". In the CallGetCategoryInfo
, I check to see whether the category being added to the List
is a "Leaf
" category - the lowest level of category (you can't drill down any further). If it is, I add that text to give a visual indication that there are sub-categories.
Notice that I didn't do anything with the various VisualStates, so this will really only look good in FullScreenPortrait
mode. I haven't really thought about how other modes would work (especially snapped!); this was more of a proof-of-concept.
You can download the entire source code here.