Silverlight TreeView Case Study - FAQ Maintenance - Part 2

by TheJet 30. August 2010 22:00

Introduction

The purpose of this series is to examine an actual implementation of a rich client user experience [UX] centered around the maintenance of a list of Frequently Asked Questions [FAQ].  In Part 1 of this series, we outlined our basic requirements, designed the data model and started building out the administrative application.  In this article we will finish building out the client application by adding Question maintenance, we’ll add some custom validation to Categories and Questions and we’ll build out the public facing component of the application.

 

Step 3 – Build Silverlight Client Administrative Application (continued)

First let’s get a quick refresher of where we are.  We have a UI layout which handles manipulating FAQ categories, including add/update/delete and drag & drop reordering.  We also have enabled change notification to the user when they select a new category and the existing category has changes.  The UI portions of question editing are also included in the XAML of the View, but we haven’t done any work to actually enable editing of Questions in the ViewModel.

 

Adding Questions

One of the features that we’d like to enable in the UI is lazy loading of Question data as the category is expanded.  We have the rudimentary requirements for that already in place, both in the retrieval of QuestionCategory instances along with the categories we are loading, and in the IsExpanded property of the TreeViewItemBase class.  We can couple these two items to present the expansion indicator [using Question Category existence], and to actually perform the load of questions [when IsExpanded changes].

Before we get into loading the questions though, we’ll need to go through and add the Question related properties to the ViewModel(s).  We’ll do that by implementing the QuestionTreeViewItem child ViewModel class, add a Questions property to the CategoryTreeViewItem and add a SelectedQuestion property to the FAQMaintenanceViewModel class, which will be used by the UI to show/hide the Question Edit UI.  Once that is complete, we can move into implementing the Lazy Load operation.

In order to facilitate the Lazy Load, we’ll override the OnExpandedChanged method of the TreeViewItemBase class on CategoryTreeViewItem.  The method will simply look to see if the item’s questions have been loaded.  If not, it will invoke the service to load the question detail by category.  Once the load comes back with data, the QuestionTreeViewItem(s) are built up to represent the Questions in the selected category. 

However, this isn’t quite enough, in order to indicate to the TreeView that there are in fact questions in need of loading, we consult the Category entity’s ‘Questions’ collection.  If there are any entries there, we add a single ‘dummy’ QuestionTreeViewItem [based on a static instance declared on the QuestionTreeViewItem class] to the CategoryTreeViewItem’s Questions collection.  This tells the TreeView that the category can be expanded, and then everything works as expected.

 

Editing Questions

Now that we can display existing questions [and add new ones], we need to handle the edit process for questions, much like we did for categories.  We want to notify the user if changes have been made, and rollback/save as required by the user.  We’ll follow a similar pattern as we used for categories to detect IsSelected changes on the tree view item and prompt the user appropriately.  The catch here is that we’re actually dealing with a hierarchy of objects that represent the question.  The object hierarchy looks like this:

QuestionCategory –> Question –> Answer(s)

Up to this point, we’ve been pretty much directly using the IEditableObject implementation and HasChanges property of the EntityObject base class to track changes.  This was already not quite enough with categories, since EntityObject.HasChanges doesn’t return ‘true’ if the entity is new.  For Questions, the interface is woefully inadequate, as it doesn’t track changes to child objects/collections at all.  So, as we did with CategoryTreeViewItem, we’ll introduce BeginEdit/CancelEdit/EndEdit methods on the ViewModel to handle edit tracking in the ViewModel itself, still relying on the underlying entities for rollback.

Answers need some additional ‘loving’ here as well, since we want to be able to delete an answer and still have the ‘Cancel’ button work to restore it.  Here we’ll leverage the SetterValueBindingHelper again to control the enabled state of the ListBoxItem instances based on an IsDeleted property on the AnswerListItem view model class.  As an aside, since we’re adding quite a bit of logic into the ‘child’ view models, I’ve also chosen to split them out into separate class files to ease maintenance.

With our new and improved Begin/Cancel/End edit changes in place, editing of the questions also works now as you might expect it to.  Removing any newly added Answers [and undeleting any removed ones] on cancel, saving changes on ‘Save’ and prompting for save based on selection changes in the tree.

 

Question Drag & Drop

If you remember from the first part of the series, I mentioned that we had to write some specialized code for Drag & Drop to handle the fact that the hierarchical data template’s ItemsSource property wasn’t being bound quite yet.  Well, that particular code is no longer necessary, as adding our Questions property on the CategoryTreeViewItem class allows the TreeView to know that it can’t drop CategoryTreeViewItems on themselves.  However, we have a similar problem on the Question side.  Before we walk through how to fix it, let’s take a moment to review what the XAML looks like to support the hierarchical binding of Category –> Question.

FAQMaintenanceView.xaml

<sdk:TreeView ItemsSource="{Binding Path=Categories}">
    <!-- Behaviors -->

    <!-- ItemContainerStyle -->

    <sdk:TreeView.ItemTemplate>
        <sdk:HierarchicalDataTemplate ItemsSource="{Binding Path=Questions}">
            <sdk:HierarchicalDataTemplate.ItemTemplate>
                <DataTemplate>

		    <!-- Question Display Contents -->

                </DataTemplate>
            </sdk:HierarchicalDataTemplate.ItemTemplate>

            <!-- Category Display Contents -->

        </sdk:HierarchicalDataTemplate>
    </sdk:TreeView.ItemTemplate>
</sdk:TreeView>

 

As you can see above, the ‘Question Display Contents’ and the ‘Category Display Contents’ are what we actually want displayed.  We could have also used templates defined in the Resources section of the page if we wanted, but the key here is that we’ve declared the TreeView’s ItemsSource to be the collection of CategoryTreeViewItems, and the ItemTemplate as a HierarchicalDataTemplate, with an ItemsSource pointing to the Questions property of the CategoryTreeViewItem.  To get the dual display feature between the Category and Question, we defined the HierarchicalDataTemplate’s ItemTemplate with a DataTemplate of how we want the Questions presented.

So that’s all well and good, but the problem is that the TreeViewDragDropTarget doesn’t like TreeView controls to be fixed depth.  We essentially have the same problem that we had before, but now it relates to the question level of the tree rather than the category level.  So, we’re going to have to adapt our Drop handler for the TreeViewDragDropTarget and take over the dropping of questions [category level drops are working fine now, and would continue to work without the existing event handler].  If you were to test the application, you would see that both questions and categories can be dropped onto QuestionTreeViewItem(s).  Categories will show up as ‘empty’, since the templating system doesn’t know how to display them, and questions will actually be rendered correctly, but as children of the question they were dropped upon.

Since we’re on the topic of Drag & Drop, we also wanted the users to be able to have questions which existed in multiple categories.  One nice way to implement this would be to leverage the ‘copy’ feature of drag & drop operations to ‘copy’ a question from one category to another.  We can do this by altering the ‘AllowedSourceEffects’ property of the TreeViewDragDropTarget to be ‘Move, Copy, Scroll’ and we’ll get all the nice drag decorators inserted for us during the drag operation.  However, the TreeViewDragDropTarget doesn’t know how to deal with Copy, so we’ll have to handle the actual copying ourselves.

To address these challenges, we’ll make some alterations to the Drop handler we defined in the last article.  The resulting code looks like this:

FAQMaintenanceViewModel.cs

public void TreeViewItemDropped(object sender, EventArgs args)
{
    // we just want Microsoft.Windows.DragEventArgs here
    Microsoft.Windows.DragEventArgs dragArgs = args as Microsoft.Windows.DragEventArgs;
    if (dragArgs == null) return;

    // determine what type of item we're dragging
    if (!dragArgs.Data.GetDataPresent(typeof(ItemDragEventArgs))) return;
    ItemDragEventArgs itemArgs = dragArgs.Data.GetData(typeof(ItemDragEventArgs)) as ItemDragEventArgs;

    Collection<Selection> itemsDragged = itemArgs.Data as Collection<Selection>;
    if (itemsDragged == null) return;

    // now, where are we going [target]?
    FrameworkElement element = dragArgs.OriginalSource as FrameworkElement;
    if (element == null || element.DataContext == null) return; // we can't know

    // only care about Question drops [or drops onto question]
    QuestionTreeViewItem questionCat = itemsDragged[0].Item as QuestionTreeViewItem;
    CategoryTreeViewItem catDragged = itemsDragged[0].Item as CategoryTreeViewItem;

    QuestionTreeViewItem destQuest = element.DataContext as QuestionTreeViewItem;

    if (catDragged != null)
    {
        // A category is being moved, are we trying to drop it on a question?
        // If so, get cancel and get out
        if (destQuest != null)
        {
            dragArgs.Effects = DragDropEffects.None;
            dragArgs.Handled = true;
            itemArgs.Handled = true;
            itemArgs.Cancel = true;
            return;
        }

        // otherwise, we're dropping onto a category, so let's just 
        // stop handling it the default behavior will work
        return;
    }

    CategoryTreeViewItem destination = element.DataContext as CategoryTreeViewItem;
    CategoryTreeViewItem source = questionCat.Parent;
    Question question = questionCat.QuestionCategory.Question;

    if (destination == null && element.DataContext is QuestionTreeViewItem)
    {
        // assume they meant the question's parent [for reordering]
        destination = ((QuestionTreeViewItem)element.DataContext).Parent;
    }

    if (destination == null || (DestinationHasQuestion(destination, question) && source != destination))
    {
        // we don't allow drops on anything other than a category
        // and they can't have the same question twice in the same destination
        dragArgs.Effects = DragDropEffects.None;
        dragArgs.Handled = true;

        itemArgs.Cancel = true;
        itemArgs.Handled = true;
        return;
    }
    else if (DragDropState == DragDropEffects.Copy)
    {
        if (source == destination)
        {
            dragArgs.Effects = DragDropEffects.None;
            dragArgs.Handled = true;

            itemArgs.Cancel = true;
            itemArgs.Handled = true;
            return;
        }
        else
        {
            SuppressSelectionChangeTracking(true);

            // we're copying, let the drop go through, but do the
            // copy at the same time.
            QuestionCategory newQuestCat = new QuestionCategory()
            {
                Category = destination.Category,
                Question = question
            };

            // Create a new question tree view item for the new 
            // category->question link
            QuestionTreeViewItem newQuest = new QuestionTreeViewItem(CurrentContext, newQuestCat, destination);

            int insertionIndex = -1;

            if (destQuest != null)
            {
                insertionIndex = destination.Questions.IndexOf(destQuest);
                if (itemArgs.DragDecoratorContentMouseOffset.Y >= (element.ActualHeight / 2))
                {
                    insertionIndex++;
                }
            }

            // place it in the source list at the index of the currently
            // selected item, so that it gets picked up instead of the
            // originally dragged item
            if (insertionIndex >= 0)
            {
                destination.Questions.Insert(insertionIndex, newQuest);
            }
            else
            {
                destination.Questions.Add(newQuest);
            }

            newQuest.IsSelected = true;

            // mark the action as handled
            dragArgs.Effects = DragDropEffects.Copy;
            dragArgs.Handled = true;
        }
    }
    else if (DragDropState == DragDropEffects.Move)
    {
        SuppressSelectionChangeTracking(true);

        // take the item out of the source
        itemArgs.RemoveDataFromDragSource();

        if (source != destination)
        {
            QuestionCategory newQuestCat = new QuestionCategory()
            {
                Category = destination.Category,
                Question = question
            };

            CurrentContext.QuestionCategories.Remove(questionCat.QuestionCategory);

            questionCat.QuestionCategory = newQuestCat;
            questionCat.Parent = destination;
        }

        int insertionIndex = -1;

        if (destQuest != null)
        {
            insertionIndex = destination.Questions.IndexOf(destQuest);
            if (itemArgs.DragDecoratorContentMouseOffset.Y >= (element.ActualHeight / 2))
            {
                insertionIndex++;
            }
        }

        // place it in the source list at the index of the currently
        // selected item, so that it gets picked up instead of the
        // originally dragged item
        if (insertionIndex >= 0)
        {
            destination.Questions.Insert(insertionIndex, questionCat);
        }
        else
        {
            destination.Questions.Add(questionCat);
        }

        questionCat.IsSelected = true;

        // mark the action as handled
        dragArgs.Effects = DragDropEffects.Move;
        dragArgs.Handled = true;
    }
    else
    {
        // other changes will be handled natively
        SuppressSelectionChangeTracking(true);
    }
}

 

I apologize for the massive code block, but as you can see there is quite a bit to handle [and yes, it could use some refactoring].  First, we want to make sure we’re handling the right type of event, in this case, only those Drop events fired using the Toolkit’s DragEventArgs class [as compared to the core Silverlight class of the same name].  Once we have that information, we gather what is being dragged [from a ViewModel standpoint] and what the item is being dropped onto.  To properly handle all the scenarios, we need to make sure that nothing can become a child of a Question, so if the user is dragging a category onto a question, we just cancel the event [we could also do the logic to, say, move the category below the questions parent, but that’s even more code].  If the user is dragging a question, then we need to know whether this is a ‘copy’ or ‘move’, and where the user dropped it, because chances are they want the question moved/copied close to that point.

Once we have all the information gathered, we proceed to do the Move/Copy [and we suppress selection changed tracking until the final submission is done on the server].  This avoids any categories/questions involved prompting the user for change confirmation during the move/copy operation.  One quick note when you are ‘cancelling’ a drop operation, you must both set the ‘Handled’ property of the DragEventArgs class to true AND set the Effects property to ‘None’, otherwise the operation will continue as if you had done nothing.

 

Custom Validation

With Drag & Drop fully functional, we can turn to our custom validation routines.  These last bits will help finalize our application and bring everything together for our client.  We’ll cover both server and client-side validation, so that we can see exactly what’s happening.

We have three different checks that we are going to implement, the first of which uses identical code on the client and server.  The ‘QuestionHasAnAnswer’ custom validation simply checks that the Question being edited has at least one answer and displays an error message if it does not.  The difficulty here is not getting the validation to happen, but rather surfacing the error to the end user. 

We would like to use the ValidationSummary control provided by the System.Windows.Controls.Data.Input assembly.  Placing the control in the same ‘parent’ container as the ‘Question Edit’ form should work properly.  However, by default, it only looks at BindingValidationError events thrown by FrameworkElement(s) in the same parent container, and there is no such object-level binding in our test form.  To solve this problem, we’ll add a hidden TextBlock element whose Text property is bound to the Question itself, with the NotifyOnValidationErrors binding property set to ‘True’.  This is sufficient to trigger the Validation Summary display as we need.

The second two validations are similar to each other, so I won’t cover them both here.  We’ll look at the ‘CategoryNameIsUnique’ validation as an example of a pattern we can use for our other client/server validators.  The goal is to do as much as possible client-side, and fall back to server-side validation during the actual submission process to avoid any unnecessary round-trips.

CategoryValidator.shared.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using FAQMaintenance.Web.Models;

#if SILVERLIGHT
using FAQMaintenance.Web.Services;
#endif

namespace FAQMaintenance.Validators
{
    public static class CategoryValidator
    {
        public static ValidationResult CategoryNameIsUnique(Category category, ValidationContext context)
        {
            IEnumerable<Category> otherCategories = null;
            
#if !SILVERLIGHT
            using (FAQMaintenanceContainer db = new FAQMaintenanceContainer())
            {
                otherCategories = db.Categories
                                  .Where(i => i.CategoryId != category.CategoryId &&
                                              i.Name.ToLower().Equals(category.Name.ToLower()))
                                  .ToList();
            }
#else
            FAQMaintenanceDomainContext domainContext = context.GetService(typeof(FAQMaintenanceDomainContext)) as FAQMaintenanceDomainContext;
            if (domainContext != null)
            {
               otherCategories = domainContext.Categories
                                 .Where(i => i.CategoryId != category.CategoryId &&
                                             StringComparer.InvariantCultureIgnoreCase.Compare(i.Name, category.Name) == 0)
                                 .ToList();
            }
#endif

            if (otherCategories != null && otherCategories.Count() > 0)
            {
                return new ValidationResult("A category with that name already exists, please choose another name.", new string[] { "Name" });
            }

            return ValidationResult.Success;
        }
    }
}

 

We’ll cover the server-side logic first, we use a new instance of the domain context to do a database lookup for any categories that match the incoming name and which aren’t the category being validated.  We place the results into a list which will be checked in the shared code.  If we find any matches, we’ll attach our error message to the ‘Name’ property and return the validation result.  We use a separate FAQMaintenanceContainer because trying to attach to the current RIA services contextual container can screw up the generated code on the server side [validations are done before the object in question is attached into the server-side context].

When we move to the client-side, things get a little more complicated.  First, in order to avoid a service call to retrieve categories, we are retrieving the current domain context with a call to the ‘GetService’ method of the ValidationContext.  To facilitate this, we need to make a small change in our base view model’s constructor when instantiating the ‘CurrentContext’ property to provide a ‘template validation context’ that the validation subsystem will use when validating entities/properties.  The relevant code looks like this:

BaseViewModel.cs

        protected BaseViewModel()
        {
            _currentContext = new FAQMaintenanceDomainContext();
            _currentContext.ValidationContext = new ValidationContext(
                                                    new object(), 
                                                    new DomainContextServiceProvider(_currentContext), 
                                                    new Dictionary<object, object>() {
                                                        {VALIDATION_CTX_CURRENT_CONTEXT_KEY, _currentContext}
                                                    });
        }

 

As you can see, we’re setting up the ValidationContext on our FAQMaintenanceDomainContext with a template instance.  The ‘new object()’ first argument is necessary to avoid a constructor exception, and ends up unused during normal validation.  The second argument is a simple provider that returns the domain context when the associated type is requested, normally you’d define an interface [service] that was returned and ask for the interface, but for this example, I avoided the extra code overhead and returned the context directly.  The final argument is a dictionary of items you want to pass to any validators that will execute.  As you can see in the example, you can also use the Items collection to pass in the current context.  Which method you choose is likely to depend on what you need to provide to your validators, the code attached at the end of the post shows how to use both approaches.

Once we have the context in hand, the validation looks much like it did on the service side, but runs entirely within the context of the client, providing much faster results and avoiding a service call for invalid data.  With the validators in place, it’s time to call our administrative client complete and move on.

 

Step 4 - Build ASP.NET page for FAQ display

With the administrative client complete, we can move on to the last step of building a simple ASP.NET page to display the FAQ data.  In this case, I’ll build out a simple web forms page to integrate with an existing website, but using ASP.NET MVC or the like is just as easy [and sometimes even simpler].  We’ll add in a simple jQuery UI Accordion control to display the categories, and use some simple Javascript to show/hide the answers to the questions.  To facilitate proper ordering of questions and answers, we’ll add some server-side extensions that just return the ordered list of QuestionCategory and Answer objects from the Category and Question respectively.  The code-behind is simply a call to the database that loads all categories, questions and answers in one shot and assigns the result to a Repeater control’s DataSource property.  The rest of the logic is in the ASPX markup and a couple lines of Javascript.  The result looks like this:

FAQ Public View

 

Summary

In this article, we walked through the addition of Question maintenance to the administrative application.  We also covered some basic client and service side validation rules and built out a public interface to display our FAQ data.  The application is fairly simple, but it allowed us to show a complete and functional Silverlight application with a rich administrative user experience.  Improving the public facing experience is just a matter of some clever styling, and the client UI gracefully degrades when JavaScript is unavailable.  We have met all our client requirements and provided the administrative staff with a pleasurable maintenance experience.

The completed code can be found here: FAQMaintenance-Part 2-20100831.zip (900 KB)

 

Conclusion

Over the course of this series of articles, we’ve built a rich client experience for administrative users.  The goal of the case study was to provide a ground up implementation of the desired features using Silverlight, and I think we’ve succeeded.  Clearly there are some things that Silverlight does well, and the Silverlight Tookit’s drag & drop capabilities certainly give us a boost when implementing basic functionality [such as list reordering].

However, enabling advanced Drag & Drop scenarios still requires that we dig a little ‘closer to the metal’ and get our hands dirty with managing the UI interactions.  Luckily, we can do so without resorting to placing a lot of code into the View.  However, we also sacrifice something on the testability front, and I’m still unconvinced that there isn’t a better dividing line that can be drawn between view and view model here to allow that testability story to stay fairly strong.  Hopefully I’ll get a chance to explore the dividing line a little more closely in future articles and derive a solution that feels a bit better.  If you have any ideas or have seen what you see as successful implementations, I’d love to hear from you.

Thanks for reading!

Silverlight TreeView Case Study - FAQ Maintenance - Part 1

by TheJet 26. August 2010 00:00

Introduction

In my last few posts, I’ve talked about using Silverlight’s Drag & Drop support to make your client user experience [UX] better.  In this series of articles, I’m going to try to pull together all three into a single coherent case study, maintaining a list of Frequently Asked Questions [FAQ].  The goal will be to provide a true ‘rich client’ experience, complete with Drag & Drop, dirty tracking, change notification and validation.  A secondary objective will be to show how we can leverage Silverlight in the more controlled ‘admin’ scenario, and then leverage the same service(s) to provide a more standard HTML/Javascript derived front end.

 

User Scenario

The user desires to maintain a list of FAQ entries, organized by category.  The same question must be able to exist within multiple categories, and the user must be able to control the order of the categories and questions within the category.  Questions can also have multiple answers, and the user would like to be able to track and order these as well.

The user does not want end users to require a special client to view FAQ information, and needs to integrate the improved FAQ listing into their existing ASP.NET website with minimal effort.  Ideally, JavaScript would not be required, and the application should provide graceful fallback to standard HTML when JavaScript is not enabled.  JavaScript can be used to enhance the user experience for those with scripting enabled.

 

Approach

In this particular case, the goal of our solution is to provide the administrative users with a rich client UX, not the public internet users.  Since the consumption of FAQ data is largely a passive activity, it does not benefit greatly from leveraging Silverlight on the client side, and a quality consumption method can be designed with minimal JavaScript.  Administrative users, on the other hand, would benefit from something beyond the ‘standard admin web site’, and we can leverage Silverlight there to provide these users with a much better overall experience, without resorting to AJAX and the like that are normally required of these types of applications.  In addition, once familiar with Silverlight development [and even when not familiar to a certain extent],  Silverlight client development can be significantly faster than ASP.NET/AJAX/MVC/etc, especially when leveraging technologies like WCF RIA Services.

So, there are a few tasks that need to be performed, and I’ll list them out here in the order that we’ll tackle them:

  1. Build Data Model
  2. Expose Data Model via RIA Services
  3. Build Silverlight Admin Client
  4. Build ASP.NET page for FAQ display

Of course, step 3 is fairly involved, so I’ll break that down when we get there, but otherwise, the tasks are all fairly straightforward.  So, let’s get started!

 

Step 1 – Build Data Model

In this case, I’m going to leverage Entity Framework 4 and a SQL Database File that’s in the App_Data folder of my website.  So to do that we’ll add a new Models folder and place our FAQMaintenance.edmx file there.  The data layout looks like this:

FAQ Maintenance Data Model

You’ll notice that we’ve captured all the data requirements above, Questions can live in multiple categories and have multiple answers, and we can control the sequence of categories, questions with the category and answers through the use of the ‘Sequence’ fields in the model.

 

Step 2 – Expose Data Model via WCF RIA Services

Once we’ve designed [and built] the web project, we can proceed to add the WCF RIA Services Domain Service to the web project.  Following my recommendations from this post, I’ve created a ‘Services’ folder in the web project, with a ‘Metadata’ folder beneath to contain any Metadata overrides [this is where we’ll put the validation attributes].  I’ll also create a ‘.extensions’ class for my domain service to house any custom service methods [remembering to add ‘partial’ to the generated class].  It’s a fair amount of work to split all these things up in this fashion, but it will serve us well as we move forward and discover that we need to change things about the service.

NOTE: One thing I am not doing in this example, and which may be a good idea for larger applications, is creating a ‘Silverlight model’ which IS NOT the actual data model and providing a mapping layer between them.  That’s too much complexity for this example, and not something I want to get into at this time.

There are a few changes we want to make here, first we’ll add some validation logic to the Categories, Questions and Answers.  The rules are pretty basic, so I’ll just list them out here.  I’ll be making the updates to the applicable ‘.metadata.cs’ files in each case.  I won’t be adding any database defined limitations, because the RIA Services client code generation will handle that for me.

  1. Questions must have at least one answer
  2. Category name must be unique
  3. Question Text must be unique within a category.

I’ll also be updating the metadata files to mark the Question and Category objects to include the ‘Questions’ and ‘Answers’ navigation properties in the return values from service calls [with, strangely enough, the ‘IncludeAttribute’].  This will allow me to make a single service call and have all data about a Category [and Question] come back in one call, rather than making multiple calls to the service.  I will split the retrieval of the data into ‘Questions’ and ‘Categories’ to demonstrate how multiple calls to the service can be handled, especially in cases where there is dependent data between the two.

I’ll also be leveraging the use of ‘shared’ classes for the Validation logic, and splitting the ‘Silverlight side’ and ‘Server side’ validation into separate code paths to keep the code together and share any that I can between the two.  The domain service ‘.extensions’ file will contain two queries, one to get the category list [with associated QuestionCategory instances], and the other to retrieve Questions by category to facilitate lazy loading of TreeView content.

With all that work done, the solution looks like this and I am able to move onto building the actual administrative client application:

Server Solution Structure

 

Step 3 – Build Silverlight Client Administrative Application

This is where the rubber meets the road, and you’ll have to forgive me for glossing over the previous bits in an effort to get here quickly.  Building the client application encompasses a number of steps, from building the basic layout to adding validation, dirty tracking/notification, etc.  We’ll start by presenting a basic layout of our target UI, then walk through adding the various features one-by-one until we have a fully working client application.

User Interface Layout

The UI for this application is fairly simple.  There are three ‘modes’, one with only the Category/Question tree shown, one in ‘Category’ edit mode, and the last in ‘Question’ edit mode.  I’ll only picture the second two here [click the picture to see a full-size version].

Category Edit Question Edit

The TreeView on the left will be populated by a hierarchy of Category –> Question nodes, and the selected node will determine which ‘screen’ is displayed on the right-hand side of the application.  The ‘Category Edit’ controls would show up if a Category was chosen, and the ‘Question Edit’ controls would appear when a Question is chosen.  Otherwise, the UI is fairly straightforward.

 

ViewModel Basic Setup

Since we’re employing the MVVM pattern here, we need to provide a ViewModel class to populate the various UI fields.  We’ll start by just populating the Categories in the list, which should give us a single-level TreeView of the Categories, and we should see the Category edit mode appear when we select one.  The code to load the Categories is fairly simple, we’ll leverage the GetCategoryList() custom method on our WCF RIA Domain Service to ensure that we get back our ‘question links’ as well for later use.

To display the categories, we won’t use a direct binding to the Model classes, but rather we’ll create a CategoryTreeViewItem class which represents a Category in the tree.  To facilitate some shared functionality for selection/expansion we’ll also create a simple ‘TreeViewItemBase’ class which represents a TreeViewItem ‘ViewModel’, then derive our CategoryTreeViewItem class from that.  A BaseViewModel class provides simple things like OnPropertyChanged implementation and simple message windowing.

To hook our ViewModel up to the View, I’ll put code in the NavigatedTo event handler on the Page class that we implemented.  This will represent the only code that we have in our View code-behind, and I use it to properly instantiate the ViewModel class based on incoming parameter values from the NavigationContext.  I’ve found this to be cleaner than an ‘Initialize’ method on the ViewModel, and it allows us to use non-default constructors for ViewModels, something that’s very handy [but we won’t use in this example].

At this point, we have a ‘fully functional’ category maintenance application.  Meaning I can load, add, save and delete categories [we’ll add our custom validation later].  I can even drag and drop the categories around for good measure.  One thing I want to draw attention to is the use of the custom ‘TreeViewBoundSelectedItemBehavior’ custom behavior that allows us to bind the ‘SelectedTreeViewItem’ property to the TreeView class to properly display the edit window, this in turn is exposed via the ‘SelectedCategory’ property [and the SelectedQuestion property we’ll add later] to allow binding of the UI elements for editing.

 

Change Notification

The only thing that this user interface doesn’t do [for categories], is detect when you’ve changed something about a category and then clicked another item in the list.  To implement that feature we’re going to turn to our ‘TreeViewItemBase’ class that I mentioned earlier.  The entirety of the code for this class is produced below:

TreeViewItemBase.cs

namespace FAQMaintenance.Client.ViewModels
{
    public class TreeViewItemBase : BaseViewModel
    {
        private bool _isExpanded;
        public bool IsExpanded
        {
            get { return _isExpanded; }
            set
            {
                if (value != _isExpanded)
                {
                    _isExpanded = value;
                    if(!SuppressExpandedChanged) OnExpandedChanged();
                    OnPropertyChanged(() => IsExpanded);
                }
            }
        }

        internal bool SuppressExpandedChanged { get; set; }

        protected virtual void OnExpandedChanged()
        {

        }

        private bool _isSelected;
        public bool IsSelected
        {
            get { return _isSelected; }
            set
            {
                if (value != _isSelected)
                {
                    _isSelected = value;
                    if(!SuppressSelectedChanged) OnSelectedChanged();
                    OnPropertyChanged(() => IsSelected);
                }
            }
        }

        internal bool SuppressSelectedChanged { get; set; }

        protected virtual void OnSelectedChanged()
        {
        
        }
    }
}

 

The goal here is along the lines of this excellent post by Josh Smith on WPF TreeView interactions.  This class is intended to handle the IsSelected/IsExpanded properties of the individual TreeViewItem classes and allow us to programmatically select items from the TreeView in code.  So, why continue to use the custom behavior mentioned above?  Primarily because I didn’t want to have to notify all the way up the ‘ViewModel’ tree that a selection had changed so that I could, in the end, do a property change notification on the SelectedCategory/SelectedQuestion properties on my main view model.  Using the two together provides for a little bit cleaner code in the end [and yes, like all things, there are ‘many ways to skin the cat’].

Unfortunately, when we try to do as Josh suggests in our Silverlight XAML, we find that we cannot.  In fact, Silverlight does NOT support bindings in the Style elements at all.  The Silverlight XAML parser pukes on such references.  Luckily for us, David Anson [aka Delay] has a ready solution in this blog post.  Using the SetterValueBindingHelper allows us to do the bindings as we’d like and everything ‘just works’.  The relevant XAML snippet is below:

FAQMaintenance.xaml

<sdk:TreeView.ItemContainerStyle>
    <Style BasedOn="{StaticResource DefaultTreeViewItemStyle}" TargetType="sdk:TreeViewItem">
        <!-- NOTE: This should work, but does not -->
        <!-- Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}" / -->
        <!-- Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" / -->
        <!-- With Delay's SetterValueHelper -->
        <Setter Property="helpers:SetterValueBindingHelper.PropertyBinding">
            <Setter.Value>
                <helpers:SetterValueBindingHelper>
                    <helpers:SetterValueBindingHelper
                        Property="IsExpanded"
                        Binding="{Binding Path=IsExpanded, Mode=TwoWay}" />
                    <helpers:SetterValueBindingHelper
                        Property="IsSelected"
                        Binding="{Binding Path=IsSelected, Mode=TwoWay}" />
                </helpers:SetterValueBindingHelper>
            </Setter.Value>
        </Setter>
    </Style>
</sdk:TreeView.ItemContainerStyle>

 

Armed with the above code, we can now handle our ‘OnSelectedChanged’ events on the TreeViewItem(s) to detect that things have changed and that a save is required.  If the save is not successful, we can jump back to the relevant TreeViewItem by setting our IsSelected property on the ViewModel and show the validation messages.  Of course, Silverlight is asynchronous, so one area this solution falls down is if the save fails for the first item, and the user is midstream editing another item.  If the second item also has validation errors the UI can get into an infinite ‘selection loop’, but we’ll ignore that edge case for this example.  A simple fix would involve just setting the IsSelected back when they say ‘Yes’ to save changes and not providing an opportunity for the error.  I’m not doing that here because following the user intention of switching TreeViewItems is more important [to me] than the edge case of dual-edits.

 

Drag & Drop Anomalies

Another issue that arises in our basic setup is that due to the above ‘edit detect’ code, dragging & dropping items in the TreeView can prove problematic.  This is because the results of the various drop actions can cause interim changes to the state of our classes, primarily in the ‘Move’ scenario, and we know that we’re going to auto-submit those changes to the server once the drag operation completes.  So, we need a way of suppressing these type of events during the move, which is where the TreeViewItemBase ‘SupressSelectedChanged’ property comes into play.  During a move, we set this property to ‘true’, perform the move(s), submit the changes and turn the property back to ‘false’ to re-enable selection changed tracking.

When testing the user interface at this stage, you’ll notice that Categories can be ‘dragged into’ other categories, and when that happens, we lose track of them in our ViewModel and we can no longer see them on the screen.  We need to provide a method of preventing categories from dropping into other categories.  To handle this, we’ll introduce a handler for the TreeViewDragDropTarget’s Drop event, and explicitly handle the Drop to move the category around in the source list.  This is a little more work than letting the eventing handle it, and if anyone knows of a better solution, please drop a note in the comments.  The handler looks like this:

FAQMaintenanceViewModel.cs

public void TreeViewItemDropped(object sender, EventArgs args)
{
            
    // we just want Microsoft.Windows.DragEventArgs here
    Microsoft.Windows.DragEventArgs dragArgs = args as Microsoft.Windows.DragEventArgs;
    if (dragArgs == null) return;

    // determine what type of item we're dragging
    if (!dragArgs.Data.GetDataPresent(typeof(ItemDragEventArgs))) return;
    ItemDragEventArgs itemArgs = dragArgs.Data.GetData(typeof(ItemDragEventArgs)) as ItemDragEventArgs;

    Collection<Selection> itemsDragged = itemArgs.Data as Collection<Selection>;
    if (itemsDragged == null) return;

    // now, where are we going [target]?
    FrameworkElement element = dragArgs.OriginalSource as FrameworkElement;
    if (element == null || element.DataContext == null) return; // we can't know

    /**NOTE:  For now, we need to handle drop events for categories directly
        *        so they don't get 'dragged' into each other, this won't be
        *        necessary once we start handling questions too...
        **/
    CategoryTreeViewItem catDragged = itemsDragged[0].Item as CategoryTreeViewItem;
    if (catDragged == null) return; // can't do anything about non-categories...

    CategoryTreeViewItem destination = element.DataContext as CategoryTreeViewItem;
    if (destination == null) return; // we're not dropping on a category

    itemArgs.RemoveDataFromDragSource();

    int insertionIndex = Categories.IndexOf(destination);

    if (itemArgs.DragDecoratorContentMouseOffset.Y >= (element.ActualHeight / 2))
    {
        insertionIndex++;
    }

    // place it in the source list at the index of the currently
    // selected item, so that it gets picked up instead of the
    // originally dragged item
    if (insertionIndex >= 0)
    {
        Categories.Insert(insertionIndex, catDragged);
    }
    else
    {
        Categories.Add(catDragged);
    }

    dragArgs.Handled = true;
}

 

If you read closely, you’ll see a comment about not needing to handle Category drags in the final version of the application.  This is due to the HierarchicalDataTemplate’s ItemSource property being ‘unbound’ at this stage in the game.  We’ve told the TreeView that we have hierarchical data, but we haven’t provided actual bound collections for the hierarchy.  As a result, we need to prevent the TreeViewDragDropTarget from ‘helping’ us by making our data hierarchical.  Once we add Question handling [in Part 2], that problem should go away on it’s own and we can get rid of this particular code.

At this point, category maintenance is complete, and in Part 2, we’ll move onto adding ‘Questions’ into the mix and hopefully get things finished up.  A version of the application with just category maintenance in play can be found for download here:

FAQMaintenance-Part 1-20100826.zip (752.32 kb)

 

Summary – Part 1

In this article, we’ve started the process of creating a full-blown FAQ management application, complete with public and administrative functionality.  Up to this point, we’ve covered the creation of the domain model, services and the start of the administrative client application.  In Part 2, we’ll complete the administrative client application by adding Question management, and provide the public facing ASP.NET web page for viewing the FAQ information.

Thanks for reading!

UPDATE:  Part 2 can be found here

Silverlight MVVM Drag & Drop – TreeView, Meet ListBox

by TheJet 17. August 2010 00:00

Introduction

In my previous two articles, I covered using Drag & Drop for both the ListBox and the TreeView controls, leveraging the Silverlight Toolkit’s ItemsControlDragDropTarget derived classes to provide a rich user experience [UX].  In this article, I want to tie the two technologies together to provide a rich UX for editing categories and an associated item, in this case, Images.  The scenario is born out of a recent Silverlight 4 application I worked on to allow the maintenance of Categories and associated Images.

In this post, I’ll be starting with the end result of my TreeView MVVM Drag & Drop article, and extending it to include a ListBox on the right half of the screen which will contain the list of images that belong to the currently selected category.  I’ll be making a few enhancements to the result of the last article behind the scenes, and I’m not going to cover all of those, just focus on the interaction between the TreeView and ListBox and the various Drag & Drop capabilities that Silverlight provides out-of-the-box with the Silverlight Toolkit.

 

The Goal

The goal of the application will be to present the TreeView of categories on the left-hand side of the screen, and a ListBox on the right-hand side which represents all images for the currently selected category.  The ListBox will allow the user to drop images into it for addition to the category, and we will also provide a link that launches a standard multi-select OpenFileDialog window to upload files.  As with the previous example, categories can be added, removed and reordered within the tree.  The images in the list can also be reordered by simple drag & drop operations and images can be dragged from the ListBox onto another category to move the image from one category to another.  For now, we’ll leave the ‘Save’ button in place, but a better UX may be to split the editing of categories/image data into dialogs and auto-save any drag & drop interactions.  We’ll provide deletion capabilities for the category and image using ‘hover icons’.  An example screenshot is shown below [no SketchFlow license here, sorry]:

 Basic Screen Layout

 

The Data Model

The data model from the last article gets a minor update with the addition of an Images entity, with reference to the Category to which it belongs.  The important pieces here are the image Name, Sequence and GalleryData fields, which hold the name of the image, order within the parent category and the associated image data respectively.  For this example, we’re going to store the image data in the database rather than worry about uploading the files to another location [and I’m not going to try to address the holy war about where the images should be ‘properly’ stored].

Entity Data Model Diagram

 

The View

The view from the previous example has been updated to support the additional ListBox, along with the appropriate Blend behaviors to link everything together.  The hover icons are implemented using of Triggers and the VisualStateManager to control the current visual state.  We’re going to continue using the TreeViewBoundSelectedItemBehavior that we used in the last article to track the currently selected TreeView item, until such point as the TreeView control supports direct binding to the SelectedItem property.  I’ll include the TreeView’s ItemTemplate below to show an example of the hover icon capabilities.

TreeView.ItemTemplate: 

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseEnter">
            <ia:GoToStateAction StateName="MouseOver"/>
        </i:EventTrigger>
        <i:EventTrigger EventName="MouseLeave">
            <ia:GoToStateAction StateName="Default"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="Mouse">
            <VisualState x:Name="Default" />
            <VisualState x:Name="MouseOver">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames 
                          Storyboard.TargetProperty="(UIElement.Visibility)" 
                          Storyboard.TargetName="HoverOverlay">
                        <DiscreteObjectKeyFrame KeyTime="0">
                            <DiscreteObjectKeyFrame.Value>
                                <Visibility>Visible</Visibility>
                            </DiscreteObjectKeyFrame.Value>
                        </DiscreteObjectKeyFrame>
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <VisualStateManager.CustomVisualStateManager>
        <ia:ExtendedVisualStateManager/>
    </VisualStateManager.CustomVisualStateManager>

    <!-- Mouse Capture Box -->
    <Rectangle Grid.ColumnSpan="2" 
                HorizontalAlignment="Stretch" 
                VerticalAlignment="Stretch" 
                Fill="Transparent" />

    <TextBlock Text="{Binding Category.Name}" Height="20" />

    <StackPanel Grid.Column="1" x:Name="HoverOverlay" 
                 Orientation="Horizontal" 
                 Visibility="Collapsed">
        <Button Content="X" Margin="10,0,0,0">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <ia:CallMethodAction TargetObject="{Binding}" 
                                          MethodName="DeleteCategory" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </StackPanel>

</Grid>

In the above code, I originally had an issue with the ‘Delete’ button being ‘unclickable’, meaning that when you hovered over the item and slid the mouse to the right to click the button, the button would disappear.  I found this to be due to the ‘Margin’ property on the button [or containing StackPanel] leaving a dead space between the two controls and causing the MouseLeave event to fire.  This was solved by throwing the transparent Rectangle behind the grid contents which ensured that the MouseLeave event wasn’t fired until the mouse actually left the content area of the TreeViewItem.

To support the dropping of images back onto the TreeViewItem(s), I added an additional Blend Trigger to the TreeViewDragDropTarget, and to support the Drag & Drop operations on the ListBox, I added the ListBoxDragDropTarget, selections from the view XAML are included below:

MainPage.xaml:

<toolkit:TreeViewDragDropTarget AllowDrop="True" 
                                HorizontalContentAlignment="Stretch" 
                                VerticalContentAlignment="Stretch">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ItemDragCompleted">
            <ia:CallMethodAction TargetObject="{Binding}" 
                                 MethodName="RealignItemParentage" />
        </i:EventTrigger>
        <i:EventTrigger EventName="Drop">
            <ia:CallMethodAction TargetObject="{Binding}" 
                                 MethodName="MoveImage" />
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <!-- ... Tree View Here ... -->

</toolkit:TreeViewDragDropTarget>

 

<toolkit:ListBoxDragDropTarget Margin="5,2,5,5" AllowDrop="True" 
                               HorizontalContentAlignment="Stretch" 
                               VerticalContentAlignment="Stretch"
                               Width="430"
                               HorizontalAlignment="Left">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ItemDragCompleted">
            <ia:CallMethodAction MethodName="ReorderImages" 
                                 TargetObject="{Binding}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <ListBox ItemsSource="{Binding SelectedCategory.Images}" 
                AllowDrop="True" 
                SelectedItem="{Binding SelectedImage, Mode=TwoWay}" 
                MinHeight="200"
                >
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="Drop">
                <ia:CallMethodAction MethodName="ImagesDropped" 
                                     TargetObject="{Binding}" />
            </i:EventTrigger>
        </i:Interaction.Triggers>

        <!-- ... ListBox Setup ... -->

    </ListBox>

</toolkit:ListBoxDragDropTarget>

 

The ViewModel

The ViewModel is where things get a little more involved.  I won’t be reproducing the entire ViewModel here, just calling out specific features/changes that enable the new functionality.  Previous TreeView functionality support can be reviewed in the TreeView MVVM Drag & Drop article. The reordering of the images in the ListBox builds on the example from my updated ListBox Timing Issue post.  To support the drag & drop of images from Windows Explorer onto the Silverlight application, the ImagesDropped method was added, this feature utilizes the ‘like signature’ feature of the Blend CallMethodAction to allow us to receive the arguments of the UI event in the ViewModel to assist in processing.

MainPageViewModel.cs:

public void ImagesDropped(object sender, EventArgs args)
{
    // only handle Windows generated drag events
    System.Windows.DragEventArgs dragArgs = args as System.Windows.DragEventArgs;
    if (dragArgs == null) return;

    if (dragArgs.Data.GetDataPresent(DataFormats.FileDrop))
    {
        // user is trying to drop 'something' on the listbox
        FileInfo[] files = dragArgs.Data.GetData(DataFormats.FileDrop) as FileInfo[];
        AddImages(files);
    }

}

 

The second parameter to the ImagesDropped method is declared as an ‘EventArgs’ type because there are two distinct DragEventArgs classes.  One provided by Silverlight 4 in the System.Windows namespace and one provided by the Silverlight Toolkit in the Microsoft.Windows namespace.  In this particular case, the System.Windows version is going to be used, so we do a quick check to make sure we are ignoring any others [like those generated by reordering images in the list].  Then the code looks for any FileDrop data, and if found, pulls out the FileInfo instances and passes them to the ‘AddImages’ method.  AddImages attempts to load the files as images, and if successful, adds them to the currently selected Category.

The other major new feature is the ability to drag an image from the ListBox onto another Category in the TreeView to move the image from one Category to another.  Again, we’ll be leveraging the ‘like signature’ capabilities of the CallMethodAction, but in this case, we only care about Microsoft.Windows.DragEventArgs, i.e. those generated by the Silverlight Toolkit’s DragDropTarget classes.  The ‘MoveImage’ method handles the movement of an image from one Category to another.

MainPageViewModel.cs:

public void MoveImage(object sender, EventArgs args)
{
    // only handle toolkit generated drag events
    Microsoft.Windows.DragEventArgs dragArgs = args as Microsoft.Windows.DragEventArgs;
    if (dragArgs == null) return;

    CategoryTreeViewItem destination = ((FrameworkElement)dragArgs.OriginalSource)
                                       .DataContext as CategoryTreeViewItem;
    if (destination == null) return;

    if (dragArgs.Data.GetDataPresent(typeof(ItemDragEventArgs)))
    {
        ItemDragEventArgs itemDragArgs = dragArgs.Data
                                         .GetData(typeof(ItemDragEventArgs)) as ItemDragEventArgs;
        if (itemDragArgs != null)
        {
            // the arguments inside the 'Data' member _should_ be a
            // Collection<Selection> instance
            Collection<Selection> items = itemDragArgs.Data as Collection<Selection>;
            if (items != null)
            {
                // now, each item in the collection should be an instance of the dragged item
                // NOTE:  We don't want to do ANYTHING with the TreeView related dropped items
                //        since the default behavior will handle that for us
                foreach (Selection item in items)
                {
                    if (item.Item is CategoryImageListItem)
                    {
                        CategoryImageListItem listItem = item.Item as CategoryImageListItem;
                        listItem.Parent.Images.Remove(listItem);
                        listItem.Parent.ReorderImages();

                        // now, add the item to the current drop target's list
                        listItem.Image.Category = destination.Category;
                        listItem.Parent = destination;
                        destination.Images.Add(listItem);
                        destination.ReorderImages();
                    }
                }
            }
        }
    }
}

 

Here we have to dig a little further to get at the information we need.  First, we use the ‘OriginalSource’ property of the DragEventArgs to get the ‘source’ of the ‘Drop’ event.  This turns out to be whatever control you happened to drop the image upon, but due to how we do our bindings in the XAML, we know that the DataContext of the thing we dropped upon will be the CategoryTreeViewItem class for that TreeViewItem instance.  Just in case that isn’t true, we dump out, since we won’t be able to complete the drop operation without the destination Category [NOTE:  If you drag and drop onto the hover button that appears, it will not work].

Once the destination is established, the code pulls out the ItemDragEventArgs instance which is encapsulated by the DragEventArgs class.  The ItemDragEventArgs has a lot more information on it about the Drag event, including the source ListBox, whether or not the item has already been removed from the source, methods to remove the item from the source, etc.  In our case, because we want to manage the ‘move’ ourselves, we only care about the ‘Data’ element, which in this case, is a Collection<> of Selection instances.  Since we only want to move images, the code checks to see that the current Selection points to a CategoryImageListItem, and then proceeds to remove the item from its current parent and add it into the destination, reordering collections as necessary.

 

Summary

We’ve seen how you can enable Drag & Drop between two DragDropTargets that do not share the same DataContext type [or same structure in general].  I’ve shown you how to enable drag & drop file import from the desktop into the Silverlight application, and upload the result to the server [in the current case, through WCF RIA Services].  The application also shows how to enable Drag & Drop reordering of the ListBox and TreeView controls, including updating the underlying model appropriately [and automated backend update is a simple change], and a method for binding the SelectedItem property of a TreeView control.

I’ve glossed over a large portion of the application in this article, primarily those pieces handled in my previous two articles, and tried to place an emphasis on the core pieces that enable the new drag & drop capabilities.  The drag & drop capabilities of Silverlight, especially when coupled with the Silverlight Toolkit are quite robust, but solutions to common problems do not always ‘jump out at you’ immediately.  Specifically the dual DragEventArgs classes can be very confusing, leading to a number of ‘white screen’ exceptions as the Blend trigger cannot find the appropriately signatured method on your ViewModel.

 

Conclusion

I’m not certain that I’m crazy about using the ‘like signature’ methods on the ViewModel class, as it would seem to decrease testability of the ViewModel classes, but Commanding doesn’t really help here either, since you still need to glean information from the source view.  In this case, it may actually be cleaner, and more testable, to put the code that captures the Drag & Drop events into the View code-behind and call the ViewModel methods with more explicit arguments.  Since I don’t have a strong focus on testability in my articles up to this point, I won’t do that here, but it’s something to consider.

The complete source code can be found here: TreeViewMeetListBox-20100817.zip (1.1 MB)

NOTE:  If you plan to run the example code, you’ll need to update the connection string in Web.config to point to your properly qualified database path (look for [[YOUR PATH HERE]]).  Thanks for reading!

Silverlight TreeView - Drag & Drop with MVVM

by TheJet 28. July 2010 00:00

Introduction

The Silverlight 4 TreeView control, when leveraged with the HierarchicalDataTemplate, allows easy binding of hierarchical data.  The Silverlight Toolkit's TreeViewDragDropTarget wrapper control provides Drag & Drop support to the TreeView.  Mixing the three together, you get pretty good out-of-the-box support for Drag & Drop reorganization of hierarchical data.  However, when searching online, I was unable to find an example of integrating this functionality with the MVVM pattern.  Since my application utilizes MVVM, I wanted to avoid putting code into the View itself.  So I began the investigation into how I could leverage the Expression interactivity libraries to facilitate the functionality that I needed in the application.  This article aims to shed some light on one possible solution to the problem that doesn’t rely on external toolkits or frameworks.

In my previous article, I talked about enabling Drag & Drop reordering of a ListBox control.  Many of the techniques I’m using here are derived from the investigation that I did for that article.  Extending the result to a TreeView control brought up a few new solutions, some of which I will likely use to update the previous article with a more robust solution that is much simpler.

Background

To setup the scenario, we have a basic Category maintenance application.  The goal of the application is to provide an administrative user with the ability to reorder the category ‘tree’, to add/remove categories and to edit details about the category in a standard master/detail layout.

Baseline UI Layout

The application will be backed by a WCF RIA Services backend, but I won’t spend much time on the details of the backend.  This article will focus on enabling the administrative scenario on the client.

The Data Model

The data model, as mentioned, is extremely simple.  A ‘Category’ entity has navigation properties to its parent and children categories, and has ‘CategoryId’, ‘Name’, ‘Description’ and ‘Sequence’ attributes.  The ‘Sequence’ attribute is used to order the categories within the parent category.  The entire EF4 diagram is shown below:

Category Data Model

 

The View

We’ll start by setting up our view and getting the basic UI layout in place based on the layout above.  Selecting a node in the Tree will bring up the ‘Edit’ screen for that node on the right-hand pane of the application.  Dragging a node around the tree will cause the node to ‘re-parent’ itself under the destination in the tree and submit any changes to the server in support of the reordering.  The view will also have a ‘Save’ button which forces a submission of changes to the server.  For the purposes of this example, I’ve left off the ‘Cancel’ button as it brings in a whole host of usage scenarios that I don’t want to address here.

I won’t reproduce the entire view code here in the interest of saving space, but I will call out the important pieces.  The actual TreeViewDragDropTarget + TreeView + HierarchicalDataTemplate that enable the drag & drop functionality are shown below:

 

        <!-- TreeView -->
        <StackPanel Grid.Row="1">
            <TextBlock Text="Categories:" />
            <toolkit:TreeViewDragDropTarget AllowDrop="True"
                                            HorizontalContentAlignment="Stretch"
                                            VerticalContentAlignment="Stretch">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="ItemDragComplete">
                        <ia:CallMethodAction TargetObject="{Binding}" MethodName="RealignItemParentage" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
                
                <sdk:TreeView ItemsSource="{Binding Path=Categories}">
                    <i:Interaction.Behaviors>
                        <helpers:TreeViewBoundSelectedItemBehavior TargetObject="{Binding}"
                                                                   PropertyName="SelectedCategory" />
                    </i:Interaction.Behaviors>
                    
                    <sdk:TreeView.ItemTemplate>
                        <sdk:HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
                            <TextBlock Text="{Binding Path=Category.Name}" />
                        </sdk:HierarchicalDataTemplate>
                    </sdk:TreeView.ItemTemplate>
                </sdk:TreeView>
            </toolkit:TreeViewDragDropTarget>
        </StackPanel>

 

A couple things here, first, the ‘AllowDrop’ property is set to True, as we want to be able to drag and drop the items within the tree.  Secondly, I’ve added a trigger to the ‘ItemDragComplete’ event.  The ItemDragComplete event is fired when the entire drag/drop process is done.  The event order is a little different with the TreeView, and it was my investigation here that will lead to a better solution for the ListBox timing issue I mentioned in my previous article.  The order of events looks like this:

  1. … various other events …
  2. [if destination is same parent node as source] ItemDroppedOnSource
  3. CollectionChanged [Remove]
  4. CollectionChanged [Add]
  5. ItemDragComplete

The key here is that my ListBox solution of setting a flag in the ItemDroppedOnSource event simply won’t work with the TreeView.  This finding led me to revisit my previous solution and I discovered that there was no need to set a flag at all, the CollectionChanged events [all of them] are taken care of during the completion of the Drop operation, so I simply needed to wait until ItemDragComplete was fired and all the collection reordering/realignment is done.  At that point, I invoke my re-alignment code to clean up the underlying Category objects and submit the changes to the server.

The second key item is the custom behavior that I wrote to handle the binding for the SelectedItem property of the TreeView class.  The problem here is that the SelectedItem property on the TreeView is read-only, which means that you cannot do a TwoWay binding as you would expect, in fact, you can’t really bind the SelectedItem property AT ALL.  If you try to do so [at least in my limited testing], Silverlight generates a fatal exception and ‘white screens’.  To work around this, I’ve adapted code from this blog post into a custom behavior that handles the binding between the ViewModel property and the TreeView property.  Hopefully future versions of the Toolkit will fix this problem.

The ViewModel

The ViewModel is pretty straightforward.  The only pieces that I’ll call out are the CategoryTreeViewItem nested class and the RealignItemParentage method.  We have a ‘child’ view model, the CategoryTreeViewItem class, which wraps the individual Category entities and serves as our ItemsSource for the TreeView.  The ViewModel handles creating the root nodes at the MainPageViewModel level, and each CategoryTreeViewItem handles populating it’s Children collection with the appropriate CategoryTreeViewItems based on its Children categories.

The RealignItemParentage method zips through the CategoryTreeViewItem(s) and sets their associated ‘Category’ children to the correct parent category and sequence.  Then a call to Save() invokes SubmitChanges to save data to the database.

 

    public class MainPageViewModel : BaseViewModel
    {

        #region 'Child' View Models
        public class CategoryTreeViewItem : BaseViewModel
        {
            public CategoryTreeViewItem()
            {
                Children = new ObservableCollection<CategoryTreeViewItem>();
            }

            public CategoryTreeViewItem(Category category)
                : this()
            {
                Category = category;

                foreach (Category child in category.Children.OrderBy(i=> i.Sequence).ThenBy(i => i.Name))
                {
                    Children.Add(new CategoryTreeViewItem(child));
                }
            }

            public Category Category
            {
                get;
                set;
            }

            public ObservableCollection<CategoryTreeViewItem> Children { get; set; }

            public void RealignItemParentage()
            {
                foreach (CategoryTreeViewItem cat in Children)
                {
                    cat.Category.Sequence = (byte)Children.IndexOf(cat);
                    cat.Category.Parent = this.Category;

                    cat.RealignItemParentage();
                }
            }

        }
        #endregion

        /// more code ...

        #region Public Methods
        public void RealignItemParentage()
        {
            foreach (CategoryTreeViewItem cat in Categories)
            {
                cat.Category.Sequence = Categories.IndexOf(cat);
                cat.Category.Parent = null;

                cat.RealignItemParentage();
            }

            Save();
        }

        /// yet more code ...

 

Conclusion

In all, linking the Silverlight Toolkit’s TreeView Drag & Drop with an MVVM structure is fairly straightforward, and I was surprised that I didn’t find more examples online in my searches.  The example code below contains a fully working application, with reordering, updates and category addition.  Adding deletion of the currently selected category should be a fairly straightforward extension that I’ll leave to the reader.

The source code can be found here: TreeViewMVVM.zip (172K)

NOTE:  If you plan to run the example code, you’ll need to update the connection string in Web.config to point to your properly qualified database path (look for [[YOUR PATH HERE]]).  Thanks for reading!

Silverlight 4 ListBox Drag & Drop Timing Issue

by TheJet 22. July 2010 00:00

UPDATE [08/17/2010]:  Version 3.0 added

Just a quick entry to talk about an issue I recently ran into involving Drag & Drop list reordering using the ListBox control.  I found numerous samples using the Silverlight Toolkit's ListBoxDragDropTarget to drag and drop an item between two ListBox instances.  After digging a bit, and running through a number of articles that talked about hacks and workarounds to make it work with a single ListBox instance, I stumbled upon an article that showed that it really was as simple as it should be:  Don't do anything special, it's supported out-of-the-box already.  How I love those type of answers.

For me the main problem in getting it to work was learning that you had to replace the default VirtualizingStackPanel with a standard StackPanel.  That was the single biggest hang-up.  A close second was learning that you can't just use the collection that is returned from the RIA services call, that was solved by simply wrapping the result in an ObservableCollection [a good idea anyway].  Then, linking it up with my MVVM implementation was fairly straightforward, once I figured out the right event to respond to with the Interactivity Triggers, I was ready to go.  The scenario was very simple, update the sequence value based on the index of the item in the list and submit changes back to the server.

Of course, nothing seems to ever be that simple :)  Or maybe I'm just lucky.  The problem I was seeing was that, very often, my reorder event was firing AND completing before the actual underlying list had been updated.  This seemed very strange, I expected the list to be updated and THEN have my event called.  The behavior I was seeing was doubly strange because it was happening ONLY on the first drag/drop action, ALL subsequent actions were working as expected.  So, on a whim, I added a bit of code to delay the method that does the reordering, and lo-and-behold, things started to 'just work'.

 

Version 1.0

The XAML

 

        <toolkit:ListBoxDragDropTarget AllowDrop="True" 
                                      HorizontalContentAlignment="Stretch"
                                      VerticalContentAlignment="Stretch">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="ItemDroppedOnSource">
                    <ia:CallMethodAction MethodName="ReorderList" TargetObject="{Binding}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
            
            <ListBox ItemsSource="{Binding Path=ItemsList}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Path=CustomerName}" />
                    </DataTemplate>
                </ListBox.ItemTemplate>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel />
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>                
            </ListBox>
        </toolkit:ListBoxDragDropTarget>

 

The piece to notice above is that I've wrapped the ListBox in a ListBoxDragDropTarget and I've made sure to enable the horizontal and vertical content alignment properties to stretch the ListBox to the bounds of the drop target. This is a common complaint that I've seen posted online, where the ListBox doesn't take up the 'whole space' that it used to before the drop target was added. Also, you'll notice that I've added a trigger on the ItemDroppedOnSource method to handle the dropping of the item on the ListBox itself.

 

The ViewModel Code

 

        private DispatcherTimer _reorderTimer;
        public ViewModel()
        {
            // ... other constructor bits

            // setup reorder timer
            _reorderTimer = new DispatcherTimer();
            _reorderTimer.Interval = new TimeSpan(0, 0, 0, 0, 300); // 300ms
            _reorderTimer.Tick += (s, e) =>
            {
                _reorderTimer.Stop();
                DoReorderList();
            };
        }

        public void ReorderList()
        {
            //***NOTE:  This is a HACK, but it seems to allow the first 
            //          drag-drop reorder to be successful
            if(_reorderTimer.IsEnabled) return; // don't allow multiple calls too quickly

            _reorderTimer.Start();
        }

        private void DoReorderList()
        {
            foreach (EntityClass item in CurrentContext.EntityClasses)
            {
                item.Sequence = (byte)ItemsList.IndexOf(item);
            }

            CurrentContext.SubmitChanges();
        }

 

Here I've just utilized the DispatcherTimer class to create a slight delay between the dropping of the item and the reset of the sequence. As mentioned in the beginning, the item dropped event seems to fire before the actual collection is rearranged, so a possible alternative solution would be to set a flag in this event, and then capture the CollectionChanged event on ItemsList and base our reorder on that flag. I'll update this post if anything comes of that experiment.

 

UPDATE: Version 2.0

After writing the post and thinking about it a bit more, I began to really feel the ‘code smell’ coming from the solution posted above.  The solution works, but it’s a HACK and I don’t like it that way.  So, after further review, I’ve come up with what I believe to be a more elegant solution that doesn’t feel like such a hack and appears to work just as well.  I’ve outlined ‘Version 2.0’ below.

 

The XAML [unchanged]

 

        <toolkit:ListBoxDragDropTarget AllowDrop="True" 
                                      HorizontalContentAlignment="Stretch"
                                      VerticalContentAlignment="Stretch">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="ItemDroppedOnSource">
                    <ia:CallMethodAction MethodName="ReorderList" TargetObject="{Binding}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
            
            <ListBox ItemsSource="{Binding Path=ItemsList}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Path=CustomerName}" />
                    </DataTemplate>
                </ListBox.ItemTemplate>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel />
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>                
            </ListBox>
        </toolkit:ListBoxDragDropTarget>

 

That's right, no XAML changes necessary here, at least I got one thing right :).

 

The ViewModel Code

 

        #region Private Fields
        private bool _reorderList = false;
        #endregion

        #region Constructors
        public EntityClassListViewModel()
            : base()
        {
            ItemsList = new ObservableCollection();
            ItemsList.CollectionChanged += (s, e) =>
                {
                    if (_reorderList && e.Action == NotifyCollectionChangedAction.Add)
                    {
                        _reorderList = false;
                        DoReorderList();
                    }
                };
            // ... other constructor code ...
        }
        #endregion

        #region Public Methods
        public void ReorderList()
        {
            // Set the reorder flag to allow collection changed to pick it up
            _reorderList = true;
        }
        #endregion

        #region Private Methods
        private void DoReorderList()
        {
            foreach (EntityClass item in CurrentContext.EntityClasses)
            {
                item.Sequence = (byte)ItemsList.IndexOf(item);
            }

            CurrentContext.SubmitChanges();
        }
        #endregion

 

Something very similar to the code above was working 'almost' right out of the gate. However, nothing seemed to be actually saving when I moved things around. Then I discovered the problem. The events that are fired look something like this:

  1. ItemDroppedOnSource
  2. CollectionChanged
  3. CollectionChanged

Of course, the problem lies in the double-collection changed event.  The ListBoxDragDropTarget code first removes the item from the collection, then inserts it into the appropriate place in the collection.  It makes perfect sense when you take a moment to think about it, but in the late night/early morning, things don’t come quite so easy :).  The fix was, as you can see above, to only actually reorder the list when the item is added back into the list.  Now, in my scenario, I don’t need to make sure my sequences are nice and tight [no gaps], so I don’t need to worry about deletions and whatnot, but if you did, you could easily rework the above code to keep a ‘no gaps’ sequence list.

With this ‘Version 2.0’ solution, things feel much less dirty and my mind is at ease :)

UPDATE [08/17/2010]: Version 3.0

After some time has passed and implementations have come and gone, the amount of code required to ‘delay’ the reordering and SubmitChanges() calls still seemed like a HACK.  It also doesn’t properly handle multi-select ListBox instances where multiple items are drag/dropped.  It turns out, in researching my TreeView MVVM article I discovered a much simpler and cleaner solution, the ItemDragCompleted event.  The updated solution is below:

The XAML

 

        <toolkit:ListBoxDragDropTarget AllowDrop="True" 
                                      HorizontalContentAlignment="Stretch"
                                      VerticalContentAlignment="Stretch">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="ItemDragCompleted">
                    <ia:CallMethodAction MethodName="ReorderList" TargetObject="{Binding}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
            
            <ListBox ItemsSource="{Binding Path=ItemsList}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Path=CustomerName}" />
                    </DataTemplate>
                </ListBox.ItemTemplate>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel />
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>                
            </ListBox>
        </toolkit:ListBoxDragDropTarget>

 

The only change here is binding the ReorderList method to the ItemDragCompleted event rather than ItemDroppedOnSource.

The ViewModel Code

 

        #region Constructors
        public EntityClassListViewModel()
            : base()
        {
            ItemsList = new ObservableCollection<EntityClass>();
            // ... other constructor code ...
        }
        #endregion

        #region Public Methods
        public void ReorderList()
        {
            foreach (EntityClass item in ItemsList)
            {
                item.Sequence = (byte)ItemsList.IndexOf(item);
            }

            CurrentContext.SubmitChanges();
        }
        #endregion

 

Here, you can see the code has been cleaned up significantly, and doesn't rely on the CollectionChanged events any longer. In retrospect, clearly I got too far 'down in the weeds' and solved the wrong problem with my Version 1.0/2.0 solutions, but hey, that’s what’s great about being a developer, always learning new and better ways to do things!

Thanks for reading!

Silverlight RIA Services - Extending an EF4 Database Model with 'POCO' entities

by TheJet 13. July 2010 06:00

Introduction

Recently when working with a Silverlight 4 RIA services client application, I stumbled upon a need to augment the RIA services with some objects that weren't derived directly from database tables or views, and I had no desire to write a SQL stored procedure to perform the complicated calculations necessary to build the return values.  Of course, RIA services doesn't particularly like serving up these types of objects, or more accurately, the tooling doesn't support such behavior out of the box.  I also didn't like the idea of throwing up a separate WCF service to serve the non-model derived objects either, as it presented two separate interfaces to the client code, rather than the single unified application service interface that RIA services provides.

I had a few requirements for the returned objects:

  1. The class should provide a navigation property to a parent object which exists in the model [client and service side]
  2. The objects would not be saved/persisted to the database
  3. The client-side representation of the parent class should have a navigation property to the child object to assist in binding the UI
  4. The class required an enumeration property

 

Experimentation

So I began walking down the path of adding some simple objects to the EF4 model which were not mapped to any underlying data store.  It turns out that, although it should be possible and can actually be compiled without errors [but with warnings], the EF4 infrastructure does not allow you to have a 'partially database derived' model.  As soon as you do so, runtime errors are thrown, even in situations where the non-database objects are not referenced.  So that pretty much landed me in a dead end.  Strike one...

I had already ruled out the use of two separate models, since I wasn't certain how RIA services would handle that, and I didn't think that navigation properties between the models would be possible.  At some point in the future, I may walk down that particular path, but likely in another form [and another post]. Strike two...

The third attempt was to have the RIA service return an IEnumerable<> of the appropriate POCO objects.  Unfortunately, there really isn't any way to generate the code around this, since the code generation tooling expects an underlying model to exist to generate the code from.  What resulted is a little ugly to look at, but it's dead simple to USE on the client-side and doesn't fall outside the normal usage of the RIA entity patterns.  I've outlined the solution below in the hopes that it can lead to an even better solution long term.

 

The Solution

Throughout the rest of the article I’ll be utilizing the ‘Chinook’ database available on CodePlex here.

The Model

We'll start with the easy part, establishing the base class which can be used for deriving all our specific POCO objects.  In this example, we'll call it PocoDataBase.  The purpose of the ‘Data’ classes will be to return the total number of milliseconds and bytes for a given artist, album, genre, etc as a summary rollup [so we’ll end up with AlbumData, ArtistData, etc].  To facilitate the retrieval of the summary data, we’ll add two views, TrackDataView and PlaylistDataView [the view creation script is here].  We’ll be doing the actual summarization in code, not in the database, to demonstrate the concept, not because it’s a good idea to do in this case.

I won’t walk you through the steps in creating a model from the Chinook data base, Chris Woodruff [@cwoodruff] made a great presentation on just this topic and the slide deck/code can be found here.  I will assume at this point that you have a baseline, RIA Services enabled project, but that you’ve only gone so far as to create the model from the Chinook database [including the two views mentioned above] and pick up from there.

First, we’ll add two PocoDataBase files, a shared one which will flow down to the Silverlight client, and another which will define the ‘base’ properties for all ‘Data’ objects.  The relevant code is here:

Models\PocoDataBase.cs

    [Serializable()]
    [DataContractAttribute(IsReference=true)]
    public abstract partial class PocoDataBase : EntityObject
    {
        /// 
        /// No Metadata Documentation available.
        /// 
        [DataMemberAttribute()]
        [KeyAttribute()]
        public global::System.Guid DataId
        {
            get
            {
                return _DataId;
            }
            set
            {
                OnDataIdChanging(value);
                ReportPropertyChanging("DataId");
                _DataId = StructuralObject.SetValidValue(value);
                ReportPropertyChanged("DataId");
                OnDataIdChanged();
            }
        }
        private global::System.Guid _DataId;
        partial void OnDataIdChanging(global::System.Guid value);
        partial void OnDataIdChanged();

        /// 
        /// No Metadata Documentation available.
        /// 
        [DataMemberAttribute()]
        public global::System.Int32 Milliseconds
        {
            get
            {
                return _Milliseconds;
            }
            set
            {
                OnMillisecondsChanging(value);
                ReportPropertyChanging("Milliseconds");
                _Milliseconds = StructuralObject.SetValidValue(value);
                ReportPropertyChanged("Milliseconds");
                OnMillisecondsChanged();
            }
        }
        private global::System.Int32 _Milliseconds;
        partial void OnMillisecondsChanging(global::System.Int32 value);
        partial void OnMillisecondsChanged();

        /// 
        /// No Metadata Documentation available.
        /// 
        [DataMemberAttribute()]
        public Nullable<global::system.int32> Bytes
        {
            get
            {
                return _Bytes;
            }
            set
            {
                OnBytesChanging(value);
                ReportPropertyChanging("Bytes");
                _Bytes = StructuralObject.SetValidValue(value);
                ReportPropertyChanged("Bytes");
                OnBytesChanged();
            }
        }
        private Nullable<global::system.int32> _Bytes;
        partial void OnBytesChanging(Nullable<global::system.int32> value);
        partial void OnBytesChanged();

        [DataMember]
        public int LengthValue
        {
            get { return _lengthValue; }
            set { _lengthValue = value; }
        }
        private int _lengthValue;

        [DataMember]
        public int SizeValue
        {
            get { return _sizeValue; }
            set { _sizeValue = value; }
        }
        private int _sizeValue;
    }

 

Models\PocoDataBase.shared.cs

    public enum Length
    {
        Unknown = -1,
        Blip = 0, // 10 seconds
        Short = 120000, // 2 minutes
        Normal = 210000, // 3.5 minutes
        Long = 300000, // 5 minutes
        Obscene = 480000 // 8 minutes
    }

    public enum Size
    {
        Unknown = -1,
        Tiny = 0,
        Small = 102400, // 100K
        Medium = 1048576, // 1MB
        Large = 5242880, // 5 MB
        Immense = 10485760 // 10MB
    }

    public partial class PocoDataBase
    {
        public Length Length
        {
            get { return (Length)LengthValue; }
            set { LengthValue = (int)value; }
        }

        public Size Size
        {
            get { return (Size)SizeValue; }
            set { SizeValue = (int)value; }
        }
    }

Then, we'll add a single AlbumData.cs class to represent our first concrete data class. This class will contain the AlbumId, Title and a reference to the server-side Album class that represents that Album.

Models\AlbumData.cs

    [Serializable()]
    [DataContractAttribute(IsReference = true)]
    public partial class AlbumData : PocoDataBase
    {
        /// 
        /// No Metadata Documentation available.
        /// 
        [DataMemberAttribute()]
        public global::System.Int32 AlbumId
        {
            get
            {
                return _AlbumId;
            }
            set
            {
                if (_AlbumId != value)
                {
                    OnAlbumIdChanging(value);
                    ReportPropertyChanging("AlbumId");
                    _AlbumId = StructuralObject.SetValidValue(value);
                    ReportPropertyChanged("AlbumId");
                    OnAlbumIdChanged();
                }
            }
        }
        private global::System.Int32 _AlbumId;
        partial void OnAlbumIdChanging(global::System.Int32 value);
        partial void OnAlbumIdChanged();

        [XmlIgnore()]
        [SoapIgnore()]
        [DataMember()]
        [Association("AlbumDataAlbum", "AlbumId", "AlbumId", IsForeignKey = true)]
        public Album Album { get; set; }

        [Browsable(false)]
        [DataMember()]
        public EntityReference<Album> AlbumReference { get; set; }


        /// 
        /// No Metadata Documentation available.
        /// 
        [DataMemberAttribute()]
        public global::System.String Title
        {
            get
            {
                return _Title;
            }
            set
            {
                OnTitleChanging(value);
                ReportPropertyChanging("Title");
                _Title = StructuralObject.SetValidValue(value, false);
                ReportPropertyChanged("Title");
                OnTitleChanged();
            }
        }
        private global::System.String _Title;
        partial void OnTitleChanging(global::System.String value);
        partial void OnTitleChanged();

    }

 

The Service

Now, we can add the actual RIA Services instance [or if you've added it already, simply delete the current one and add a new one. Remember to recompile your project before adding the service, or you won't see any changes you've made. During generation of the RIA service [Domain Service], you won't see the PocoDataBase and AlbumData entities, since they don't exist in your model proper. Generate the service [I called mine ChinookDomainService], and then re-build the project.

SIDEBAR: At this point, it's probably worth mentioning my previous article on RIA Services code [re]generation. The article helps to explain some of the organization concepts that you'll see in the following explanation.

One thing you'll notice if you immediately compile the application is that it doesn't compile. That's because we've told the RIA services infrastructure to copy our PocoDataBase.shared.cs file down the client, but then we never told it to expose the PocoDataBase class in the first place, so there is nothing for the .shared.cs class to extend. This is solved in at least two ways, the first is to create the 'PocoDataBase' entity in your model and do the song-and-dance that's required to actually make the model compile successfully [this will have the side effect of having the PocoDataBase and AlbumData classes appear in the RIA Services generation wizard]. This is the approach I took in my earlier project(s), but as mentioned in the investigation portion above, that gets too complicated [and it doesn't really express the intent that the class doesn't really exist in the database model], so I've opted to take a second approach. The second approach is to simply add a dummy method to the ChinookDomainService.extensions.cs file to expose an IQueryable returning method. This will be sufficient to get everything to compile for now.

The next step is to expose our AlbumData class to the Silverlight client so we can retrieve the rolled up summary information for display on our simple UI. To do this, we need to expose a method to retrieve the summary information, for this example, I'll provide a simple method that returns all summary information for all albums. The method looks something like this:

Services\ChinookDomainService.extensions.cs

        public IEnumerable<AlbumData> GetAlbumData()
        {
            var trackData = ObjectContext.TrackDataViews;

            List<AlbumData> retVal = new List<AlbumData>();
            foreach (int albumId in trackData.Select(i => i.AlbumId).Distinct())
            {
                retVal.Add(GetAlbumData(albumId, trackData.Where(i => i.AlbumId == albumId)));
            }

            return retVal;
        }

        private AlbumData GetAlbumData(int albumId, IEnumerable<TrackDataView> tracks)
        {
            const int AVERAGE_TRACKS_PER_ALBUM = 9;

            AlbumData retVal = new AlbumData() { DataId = Guid.NewGuid(), AlbumId = albumId, Bytes = 0, Milliseconds = 0 };

            foreach (TrackDataView track in tracks)
            {
                retVal.Title = track.AlbumTitle;
                retVal.Bytes += track.Bytes;
                retVal.Milliseconds += track.Milliseconds;
            }

            // Assign Length Enumeration
            if (retVal.Milliseconds < (int)Length.Short * AVERAGE_TRACKS_PER_ALBUM) retVal.Length = Length.Blip;
            else if (retVal.Milliseconds < (int)Length.Normal * AVERAGE_TRACKS_PER_ALBUM) retVal.Length = Length.Short;
            else if (retVal.Milliseconds < (int)Length.Long * AVERAGE_TRACKS_PER_ALBUM) retVal.Length = Length.Normal;
            else if (retVal.Milliseconds < (int)Length.Obscene * AVERAGE_TRACKS_PER_ALBUM) retVal.Length = Length.Long;
            else retVal.Length = Length.Obscene;

            // Assign Size Enumeration
            if (retVal.Bytes < (int)Size.Small * AVERAGE_TRACKS_PER_ALBUM) retVal.Size = Size.Tiny;
            else if (retVal.Bytes < (int)Size.Medium * AVERAGE_TRACKS_PER_ALBUM) retVal.Size = Size.Small;
            else if (retVal.Bytes < (int)Size.Large * AVERAGE_TRACKS_PER_ALBUM) retVal.Size = Size.Medium;
            else if (retVal.Bytes < (int)Size.Immense * AVERAGE_TRACKS_PER_ALBUM) retVal.Size = Size.Large;
            else retVal.Size = Size.Immense;

            return retVal;
        }

Because the method exposes an IEnumerable it automatically gets considered a 'Query' type method. So accessing it from the client is as simple as calling 'DomainContext.Load(DomainContext.GetAlbumDataQuery(), ...)'. However, right away you'll encounter a couple compilation errors when you try to build the solution. One relates to the underlying WCF technology used with RIA Services, we need to tell WCF that we can send/receive PocoDataBase instances which are actually AlbumData instances. We do this through the 'KnownType' attribute on the server-side PocoDataBase class. The updated class declaration looks like this:

    [Serializable()]
    [DataContractAttribute(IsReference=true)]
    [KnownType(typeof(AlbumData))]
    public partial class PocoDataBase : EntityObject
    {

The second is a new error that I hadn't encountered on previous projects, and may relate to the fact that my PocoDataBase class does not actually exist on the EF4 model. This error relates to the fact that the 'AlbumData.AlbumReference' property on the service side is not a supported type for client-side code generation. This can be solved easily enough by introducing some RIA services metadata for the AlbumData class. I'm not going into detail about the metadata infrastructure in this post, so I'll just post the two metadata classes that are needed here. IMPORTANT: These classes should be in the 'Models' namespace to work properly.

Services\Metadata\PocoDataBase.metadata.cs

    [MetadataType(typeof(PocoDataBase.PocoDataBaseMetadata))]
    public partial class PocoDataBase
    {
        internal class PocoDataBaseMetadata
        {
            protected PocoDataBaseMetadata()
            {

            }

            public long Bytes { get; set; }

            public Guid DataId { get; set; }

            public int LengthValue { get; set; }

            public long Milliseconds { get; set; }

            public int SizeValue { get; set; }
        }
    }

 

Services\Metadata\AlbumData.metadata.cs

    [MetadataType(typeof(AlbumData.AlbumDataMetadata))]
    public partial class AlbumData : PocoDataBase
    {
        internal sealed class AlbumDataMetadata
        {
            private AlbumDataMetadata()
            {

            }

            public Album Album { get; set; }

            [Exclude]
            public EntityReference<Album> AlbumReference { get; set; }

            public int AlbumId { get; set; }

            public string Title { get; set; }
        }
    }

The key here is the 'Exclude' attribute, which tells RIA services to avoid generating code for the service-side property on the client. We don't need it here [and don't really need it on the server in this example, but some times it is necessary, so I'll leave it in]. Once we've made these two changes, everything should compile properly. Of course, now we need some way of accessing that 'AlbumData' type from an individual Album class, and to do that, we'll introduce some client-side extensions to the Album class [and the AlbumData class].

 

The Client

On the client-side, we'd like to have a property that can navigate from the Album class to the associated AlbumData class [if loaded] and which gets notified when the data is loaded, since we'll be loading the Album instances and the AlbumData instances from two different service calls. To do this, we'll introduce some client-side extensions to the Silverlight project. The first is the simple one, we want to have a concept of an 'Unknown' AlbumData instance which can be returned by the Album instance before any AlbumData has been loaded. To accomodate this, we'll add a very simple extension to the client:

Models\AlbumData.extensions.cs

    public partial class AlbumData
    {
        public static AlbumData Empty = new AlbumData() { Length = Length.Unknown, Size = Size.Unknown };
    }

This and all other extension classes should reside in the '[Project].Web.Models' namespace, so that the partials match up with the RIA Service generated code. The second extension is a little more complicated, as it sets up the navigation property between the Album and the AlbumData instances. The code looks like this:

Models\Album.extensions.cs

    public partial class Album
    {
        private EntityRef<AlbumData> _data;

        [Association("AlbumAlbumData", "AlbumId", "AlbumId", IsForeignKey = false)]
        [XmlIgnore()]
        public AlbumData Data
        {
            get
            {
                if ((this._data == null))
                {
                    this._data = new EntityRef<AlbumData>(this, "Data", this.FilterAlbumData);
                }

                if (this._data.Entity == null)
                {
                    return AlbumData.Empty;
                }
                else
                {
                    return this._data.Entity;
                }

            }
            set
            {
                AlbumData previous = this.Data;
                if (value != previous)
                {
                    _data.Entity = value;
                    RaisePropertyChanged("Data");
                }
            }
        }

        private bool FilterAlbumData(AlbumData entity)
        {
            return (entity.AlbumId == this.AlbumId);
        }

    }

This extension provides our navigation property between the Album and the AlbumData instance that represents our summary information. Now we can bind the UI to to Album.Data.Size and get proper change notifications, etc when the data is loaded asynchronously. The last step is actually hooking up the call into the service to retrieve the information. For example:

ViewModels\MainPageViewModel.cs

        public MainPageViewModel()
        {
            // ... other code

            CurrentContext.Load(
                CurrentContext.GetAlbumDataQuery(),
                LoadBehavior.MergeIntoCurrent,
                (loadOp) =>
                {
                    if (!loadOp.HasError)
                    {
                        foreach (AlbumData data in loadOp.Entities)
                        {
                            // only tie in the AlbumData class if the 
                            // associated Album is already in context
                            if (data.Album != null)
                            {
                                data.Album.Data = data;
                            }
                        }
                    }
                },
                null);
        }

When you run the application, the albums are now loaded, and the album data instances are loaded in the background. Currently, the application loads everything at once, so it's hard to see the stuff 'flowing in' over time. I've avoided extending the application in many other ways, to avoid complicating the issue, but the concept could be extended to provide ArtistData, GenreData, etc.

Conclusion

So, how did we do in terms of the goals laid out in the beginning of the post? 

  1. The class should provide a navigation property to a parent object which exists in the model [client and service side] -- CHECK
  2. The objects would not be saved/persisted to the database -- CHECK [freebie]
  3. The client-side representation of the parent class should have a navigation property to the child object to assist in binding the UI -- CHECK
  4. The class required an enumeration property -- CHECK

To be clear, I'm not HAPPY with this particular approach, and I think, in the future, I'll likely forsake exposing database derived models through RIA services, in favor of a 'two model' approach with mapping between the Silverlight 'application service' and the underlying database service.  There will be extra code to write certainly, but it provides a nice 'cushion' to handle these types of cases.  Or maybe EF5 will let us mix POCO and database derived entities in the same model without the headaches that we see in EF4.

In the time since preparing and writing up this post, I've stumbled upon referenced data contexts, which may allow the behavior I desire without resorting to the amount of hand coding necessary here.  There isn't much that can be done about the enumerations, until they are given first-class treatment in Entity Framework, they will always need to be hacked in one fashion or another.  I continue to believe that there must be SOME way of doing what I want more simply, and I'm sure I'll find it or see it just after I post this :) [please let me know in the comments if you have better approaches that you've used successfully].

Thanks for reading! Sample code can be found here:

RIAServicesWithPoco.zip (256KB)

WCF RIA Services Regeneration - Can we get a better story please?

by TheJet 29. June 2010 08:00

I've been working with the WCF RIA Services framework in Silverlight 4 now for a few months.  The theory behind it is very interesting, and I can certainly see how it gives that feeling of a true 'RIA' framework.  In general, so long as you 'stick to the path', everything just works, which is nice for once.  There certainly are problems, and I won't go into them all here.  I certainly don't have time to detail the many and varied complaints about the metadata classes that make validation work, nor do I have time to wonder why the generated domain service itself is not marked as 'partial'.  What I would like to do is add my voice to the hundreds of others who are asking, 'Why can't regeneration work?'.  Of course, just doing that is boring, so I'll make up a badly written narrative of a lonely developer working his way through his first RIA project.

DISCLAIMER:  No code monkeys were harmed in the creation of this story, the story and any project details are entirely fictional and do not represent a real world project in any way, shape or form.


Happy Times

When George first started this RIA project, he happily built his data model and added that first domain service to the project.  Everything generated beautifully, he built out the first pass at the application and all was right with the world.  George wondered about these metadata classes, but they seemed usable, if somewhat weird and constraining.  So he went about the business of adding validation rules to the metadata classes and marveled at the ease in which those error messages appeared on the UI, almost as if by magic, as he had done nothing but setup appropriate binding [remembering to include those NotifyOnValidationError and ValidatesOnExceptions attributes].

The customer was happy, George was happy, and then the first official review arrived.  Turns out George's data model was off, just a bit, in just a few places.  Oh, and the customer needed these few things added over there.  There was also this entity that marketing never mentioned, but now that they see things, they'll definitely need that.  "No problem," George says, "that's what these reviews are for, and we're happy to make those adjustments.  Our agile process allows us to do that with minimal longterm project impact."  George leaves the meeting refreshed and ready to tackle the next few weeks.

 

The 'Inbetween'

George returns to his desk ready to go.  The database changes are knocked out without a hiccup, and the update of the model from the database goes similarly smoothly.  There's a few weird things, and he forgets to delete a couple fields that moved around, but overall no major headaches.  Everything is looking good, the Silverlight client app is updated to accommodate the changed fields and everything is running smoothly... until he goes to add a child grid to maintain that new entity that was discovered.

 

The Story Falls Apart

"Of course", he thinks, "I've got to make sure I've generated the necessary methods on my service to handle the new entity.  What a silly thing to forget, no problem, the first time was a breeze, I'll just update things and get back to work."  Then George spends the next 30 minutes searching through Visual Studio to find the button/configuration file/etc that needs to be updated to trigger regeneration.  After 15 minutes, he's convinced he won't find anything, but he can't believe this particular use case wasn't handled out of the box.  After 30 minutes he gives up and then tries to decide how in the world he's going to maintain all the customizations that he has done while still generating the new code.  He eventually decides to just hand-code any updates that are necessary, using a couple of the other classes as templates.  The work is slow and tedious, but after a few attempts and some false starts, he manages to get everything working again.  In total, near half a day spent making updates that should have taken less than 1 hour of time.  He's back on track and moving forward, but he shutters to think about the next time he has to make changes, something virtually guaranteed by the agile processes he has used in the past so successfully...

 

Epilogue

So, what can we learn from George's story here?  I think the first take home is that the RIA services 'regeneration' story is not a good one.  Given the amount of code and logic behind the original generation process, it seems pretty silly to have left out any hooks to enable this process natively.  Visual Studio provides loads of extension points that automatically rebuild your services from a standard, configurable 'source' [the EF4 designer/code generation process is one such example].  The RIA Services 'Domain Service' wizard already requires that your solution compile, so why doesn't it produce something akin to the edmx files that EF4 uses which are then used to generate your services at compile time?

I, obviously, cannot answer that question, and I truly hope that Microsoft is working hard to improve the end user experience of RIA services to address some of these problems.  They have a site setup to handle 'feature requests' for RIA Services, and there are a few potential candidates that would make our lives easier:

And this isn't an exhaustive list, just a few that I picked out from the top vote getters.  This is a problem that needs to be solved, it's a giant pain to deal with now, but I'll walk through some steps that we've taken on projects to help alleviate the pain.

 

Alleviating the Pain

There are a few concrete steps that you can take to help alleviate the pain caused by the lack of regeneration capabilities.  Whether they end up being more or less work than hand coding will largely depend on the scope of the changes you need to make.  We use a combination of these methods and hand-coding to get the job done now.  There is anecdotal evidence that the methods below don't always work, and sometimes a restart of visual studio, or a full check-in/check-out cycle may be needed to trigger all the right things to happen.

Isolate custom metadata into a sub-directory, and use a 'single-class per file' approach

When regenerating, any existing metadata classes will NOT be regenerated, so putting them in their own directory allows easy 'Include/Exclude from Project' to force regeneration of all metadata.  Putting classes in their own file allows easy 'Include/Exclude' of a particular file to force metadata regeneration.  We usually use '<EntityName>.metadata.cs'.  Don't forget to put these classes in the 'Model' namespace, regardless of their location, so they match up with the EF4 generated classes.

Isolate 'extensions' to entities from metadata

When regenerating, it is necessary for your application to successfully compile.  While metadata updates do not generally result in compilation errors, extension updates generally do result in compilation errors.  So isolating these in their own file, which can be retained while also temporarily excluding metadata allows the regeneration process to proceed easily.  We usually use '<Entity>.extensions.cs'.  Don't forget to put these classes in the 'Model' namespace, regardless of their location, so they match up with the EF4 generated classes.

Isolate service extensions into their own file

When building RIA services, you invariably will want to add custom service methods to facilitate custom searches, loads, invokes, etc.  Even though the main DomainService derived class does NOT by default generate with the 'partial' modifier, it is simple to add and allows much greater 'regenability' by streamlining the process of regenerating the bulk of the service code.  We usually use '<ServiceName>DomainService.extensions.cs'.

'Regenerate' Code using existing Wizard

You can use the existing wizard to regenerate code, you just need to act as if you're creating a new service.  The steps we use are:

  1. Exclude any metadata classes that need to be regenerated [or the whole folder to get them all]
  2. Clean and Rebuild the solution.  This step is crucial, as it ensures that regeneration will function appropriately.
  3. Add a 'new' Domain Service to the project with the same name and a '.temp.cs' extension
  4. In the resulting dialog, clear the '.temp' from the service name and select the entities you would like to include in the service [yes, it's a pain to reselect them all every time].
  5. Copy the resulting output from the <ServiceName>DomainService.temp.cs file into the <ServiceName>DomainService.cs file, remembering to re-add the 'partial' modifier to the service class.
  6. Merge the resulting output from the <ServiceName>DomainService.temp.metadata.cs file into the classes in the 'Metadata' folder.
  7. Delete the two new .temp.cs files that were generated.
  8. 'Include' all files excluded in Step #1.
  9. Clean and rebuild.

The above steps accomplish a few things.  They allow us to retain any custom changes we've made.  They allow us to get the 'latest' version of the metadata and potentially save some typing in exchange for gratuitous cut-and-paste.  They allow source control branching/merging to work as desired, because we aren't deleting/re-adding files to the project that we care about [i.e. the .temp.cs files are never checked in, file changes are content based].  They keep the various concerns [core entities/service, metadata, extensions] separate and 'more maintainable' [to some folks].

 

Conclusion

As mentioned at the start, this story needs to be improved.  I'm hopefully that future versions of RIA services will improve on these areas, but in the mean time, we'll have to continue to find work arounds and 'best practices' that allow us to be productive using these tools and the agile processes that normally run our projects.  If you have additional techniques that you've used, things that I've missed, or just want to add your voice to the issue, please leave your comments below.

Implementing a CCTray Transport Extension for Team Foundation Server - Part 2

by TheJet 4. March 2010 20:00

Where are We?

In Part 1 I talked about the steps necessary to get a Transport Extension linked into CCTray application.  The ITransportExtension interface handles plugging our Team Foundation Server [TFS] extension into CCTray, but it does nothing to actually monitor builds running within the TFS infrastructure.  Actual server and project integration are handled through the ICruiseServerManager and ICruiseProjectManager interfaces.  In this post, I'll walk through the process of attaching to the build portions of TFS and exposing the results to CCTray.

The Team Foundation Server Client API

Before I get into the details of the CCTray integration, let's take a step back and look at what we need to do.  We really have two main tasks, first we need to retrieve the list of available build projects for a given Team Foundation Server and Team Project.  Microsoft helpfully provides a number of client API libraries for use when integrating with TFS.  To retrieve the list of build projects from a server, we'll utilize the Microsoft.TeamFoundation.Client and Microsoft.TeamFoundation.Build.Client namespaces.  The TeamFoundationServer and TeamFoundationServerFactory classes provide a method to connect into the TFS instance, and the IBuildServer interface provides methods to retrieve information about the builds defined on the server.

  // Instantiate a TeamFoundationServer through the factory
  TeamFoundationServer tfsServer = TeamFoundationServerFactory.GetServer(settings.ServerName);

  // Retrieve the IBuildServer service
  IBuildServer tfsBuildServer = tfsServer.GetService(typeof(IBuildServer)) as IBuildServer;

  // Retrieve the lits of projects available on this build server
  IBuildDefinitionSpec defSpec = tfsBuildServer.CreateBuildDefinitionSpec(Settings.TeamProject);
  IBuildDefinitionQueryResult query = tfsBuildServer.QueryBuildDefinitions(defSpec);

  IBuildDefinition[] definitions = query.Definitions;

Once we have the list of projects in the form of IBuildDefinition instances, we can query for specific details about a particular build, details about all currently running/queued builds, etc.  For example:

  IQueuedBuildSpec queueSpec = tfsBuildServer.CreateBuildQueueSpec(Settings.TeamProject);
  IQueuedBuildQueryResult result = tfsBuildServer.QueryQueuedBuilds(queueSpec);

  IQueuedBuild[] builds = result.QueuedBuilds;

  // Retrieve the build definition
  IBuildDefinition definition = ...;

  // Query for recent historical builds for this definition
  IBuildDetailSpec querySpec = tfsBuildServer.CreateBuildDetailSpec(Settings.TeamProject, definition.Name);
  querySpec.MaxBuildsPerDefinition = 10;
  querySpec.QueryOrder = BuildQueryOrder.FinishTimeDescending;

  IBuildQueryResult queryResult = tfsBuildServer.QueryBuilds(querySpec);
  IBuildDetail[] buildDetails = queryResult.Builds;

Again, we see the combination of building the query specification and executing the resulting query against the build server.  Similar methods exist to query most aspects of the build server, both for active and historical data.  The IBuildDetail interface shown above is the interface through which much of the project specific interactions will be performed.

Now that we've got a little background on what APIs are used for accessing the TFS build information, let's take a look at how we surface that information to CCTray.

The ICruiseServerManager Interface

The first step in surfacing TFS build information to CCTray is to implement the ICruiseServerManager interface.  This interface handles connecting to the TFS server, retrieving the list of available projects and returning a snapshot of the current 'server state' to the CCTray application.  The interface is shown below:

The ICruiseServerManager Interface

The TFSServerManager class implements the ICruiseServerManager interface and provides the first integration point with TFS.  Our ITransportExtension implementation that was built in Part 1 returns an instance of this class from the ITransportExtension.GetServerManager method.  In the case of TFS, the 'SessionToken' property is unused, as are the CancelPendingRequest and Logout methods.  Technically, the Login method can be skipped as well, since the TFS integration components will utilize the current windows authentication to connect to the TFS server.  To repeat, if you cannot use your current windows login context to login to the TFS server, this extension will not work as described.  Additional work would need to be done to authenticate with the TFS server.

The two key methods on this interface are GetCruiseServerSnapshot() and GetProjectList(), both of which return information about the currently connected server in slightly different formats.  We'll start with GetProjectList() since the implementation is pretty straightforward.  We're going to borrow the code outlined in the TFS API section above to retrieve the set of projects exposed by the connected TFS server and team project, then translate the resulting array of IBuildDefinition instances into CCTrayProject instances.

  public CCTrayProject[] GetProjectList()
  {
      IBuildServer buildServer = TfsServer.GetService(typeof(IBuildServer)) as IBuildServer;
      if (buildServer != null)
      {
          IBuildDefinitionSpec defSpec = buildServer.CreateBuildDefinitionSpec(Settings.TeamProject);
          IBuildDefinitionQueryResult query = buildServer.QueryBuildDefinitions(defSpec);

          if (query != null)
          {
              CCTrayProject[] projects = new CCTrayProject[query.Definitions.Length];

              for (int i = 0; i < query.Definitions.Length; i++)
              {
                  projects[i] = new CCTrayProject()
                                  {
                                      BuildServer = Configuration,
                                      ExtensionName = Configuration.ExtensionName,
                                      ExtensionSettings = Configuration.ExtensionSettings,
                                      ProjectName = query.Definitions[i].Name,
                                      SecuritySettings = Configuration.SecuritySettings,
                                      SecurityType = Configuration.SecurityType,
                                      ServerUrl = Configuration.Url,
                                      ShowProject = query.Definitions[i].Enabled
                                  };
              }

              return projects;
          }
      }
 
      return null;
  }

Once we are returning the list of projects, we can move onto the more detailed GetCruiseServerSnapshot() method.  This method will return not only the current state of all the builds, but also a snapshot of the build queues that are available on the server.  For this extension, I decided to consider each 'Build Agent' to have a queue of its own.  So a server with multiple build agents will display as having multiple queues.  The GetCruiseServerSnapshot code is a little too involved to include in its entirety, and I've covered how to retrieve the list of recent builds for a build definition in the TFS API section, but one interesting feature that I decided to utilize as the ability to 'hook into' a running build and get 'live' status updates.  The relevant code looks something like this:

  private bool AttachToNextBuild(string buildDefinition)
  {
      IQueuedBuildSpec queueSpec = _manager.TfsBuildServer.CreateBuildQueueSpec(_manager.Settings.TeamProject);
      queueSpec.DefinitionSpec.Name = buildDefinition;
      queueSpec.QueryOptions = QueryOptions.All;
      queueSpec.Status = QueueStatus.All;

      IQueuedBuildQueryResult queryResult = _manager.TfsBuildServer.QueryQueuedBuilds(queueSpec);
      if (queryResult != null && queryResult.QueuedBuilds.Length > 0)
      {
          IBuildDetail buildDetail = queryResult.QueuedBuilds[0].Build;
          if (buildDetail != null)
          {
              buildDetail.StatusChanged += new StatusChangedEventHandler(Build_StatusChanged);
              buildDetail.Connect();
              return true;
          }
      }

      return false;
  }

  void Build_StatusChanged(object sender, StatusChangedEventArgs e)
  {
      IBuildDetail buildDetail = sender as IBuildDetail;
      if (buildDetail != null && e.Changed)
      {
          // something has changed so we need to update the project status
          if (buildDetail.Status != BuildStatus.InProgress)
          {
              // we're in some sort of completion state
              buildDetail.StatusChanged -= new StatusChangedEventHandler(Build_StatusChanged);
              buildDetail.Disconnect();
          }

          lock (_statusCacheLock)
          {
              ProjectStatus oldStatus = new ProjectStatus();
              if (_projectStatusCache.ContainsKey(buildDetail.BuildDefinition.Name))
              {
                  oldStatus = _projectStatusCache[buildDetail.BuildDefinition.Name];
                  _projectStatusCache.Remove(buildDetail.BuildDefinition.Name);
              }

              ProjectStatus newStatus = _manager.GetProjectStatus(buildDetail, oldStatus);
              _projectStatusCache.Add(buildDetail.BuildDefinition.Name, newStatus);
          }

          if (buildDetail.Status != BuildStatus.InProgress)
          {
              AttachToNextBuild(buildDetail.BuildDefinition.Name);
          }
      }
  }

When the 'Connect()' method is called on the IBuildDetail instance, the StatusChanged event is fired when build status changes.  This event sets up internal polling within the TFS API [the default interval is 5 seconds] against that build, when the polling interval elapses, the API determines if the build data has changed, and if so updates the current IBuildDetail instance with the new information, which the extension then uses to create a new ProjectStatus object to expose to CCTray.

Sidebar: It should be noted that there is at least one 'anomaly' with the current codebase.  When a build includes some continuous integration unit tests, the TFS API seems to indicate that the build is no longer 'InProgress' and as such we disconnect from the build and the resulting status in CCTray shows up as whatever the project status was on the last build completion.  Once the build does actually complete, the status then updates to the correct final status.  This has caused us some issues in our red-yellow-green workflow, and is something that I'm actively looking into to see if there are additional status fields that I need to query to avoid this situation.  When I track down an answer, I'll be sure to post about it.

Once we have our ICruiseServerManager implementation in place, we have essentially enabled project tracking by CCTray without doing any additional work.  However, we would not get any of the functionality to trigger builds, view build details, etc that are enable through the various right-click options for a given project.  To enable those features, we need to look at the ICruiseProjectManager interface.

The ICruiseProjectManager Interface

The ICruiseProjectManager interface provides CCTray with project specific integration capabilities, including the ability to force a build, view the build details and cancel an in progress build.  The interface looks like this:

Team Foundation Server 2008 doesn't support some of the features exposed by the CCTray interface, for this extension, I've decided to essentially treat 'Build' and 'Project' equivalently.  So both the start and stop features of projects actually start/stop the current build.  As mentioned, cancellation of pending requests doesn't work, as all requests are currently synchronous.  There are also features to support retrieving build package data and 'file transfer' information.  In our case, these features aren't used and either throw NotImplementedException instances, or simply return NULL. 

The interesting function here is 'ListBuildParameters()' which is the first part of a 'ForceBuild' in the normal process flow.  Normally, we would throw up a dialog of some sort to collect the relevant parameters and return them here so they can be used to start the build process.  Unfortunately, there is a slightly unpleasant side effect of doing this.  It would disable the 'StartProject' functionality for our extension, which wouldn't be much of a problem, if it weren't for the fact that the menu entry would still be displayed [more on this problem in a future article].  So, rather than code up a dialog and query the TFS APIs directly, I've chosen to take a bit of a shortcut. 

A little reflection against the libraries deployed with Team Explorer nets the handy Microsoft.TeamFoundation.Build.Controls namespace, which is largely internal.  There is an extremely handy dialog hidden in there that allows for the queuing of a new build, complete with selection of build agent and other details.  This dialog will be familiar to those of you who use Team Explorer to queue up new builds, and is, ironically, used by the Team Foundation Power Tools' Build Notification application that I mentioned in the first part of this article. :)

Unfortunately, since this dialog is internal, we need to do a little reflection to retrieve an instance of the dialog.  Everything after that works 'out-of-the-box' to queue up a new build, without any interaction from our client.  The sticky part is that we must include a reference to a 'private' assembly of the team explorer and include it in the deployment package for our extension.  Which is why you won't see a download of a setup project to install this extension into a working copy of CCTray 1.5 RC1, and why you must have Team Explorer installed on your the machine you use to build a local copy of the extension for everything to work.  For those looking for many of the TFS client libraries that Team Explorer uses, you can find them in your [Program Files]\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies folder.

It would be great if Microsoft would choose to expose a few more of these controls and helpers for public consumption, and I hope to do a 'Part 3' of this article addressing how we could remove this final dependency on 'private' implementation details at some point in the future.  For those interested, the necessary code to interface with this dialog is below:

  public void ForceBuild(string sessionToken, Dictionary parameters)
  {
      System.Reflection.Assembly dlgAsm = typeof(BuildPolicy).Assembly;
      Type dlgType = dlgAsm.GetType("Microsoft.TeamFoundation.Build.Controls.DialogQueueBuild", false);

      if (dlgType != null)
      {
          Form buildDlg = default(Form);
          try
          {
              string teamProject = _serverManager.Settings.TeamProject;
              IBuildDefinition buildDefinition = _serverManager.TfsBuildServer.GetBuildDefinition(teamProject, _projectName);

              IBuildAgent[] agents = _serverManager.TfsBuildServer.QueryBuildAgents(_serverManager.TfsBuildServer.CreateBuildAgentSpec(teamProject)).Agents;

              object[] args = new object[] { teamProject, new IBuildDefinition[] { buildDefinition }, 0, agents, _serverManager.TfsBuildServer };
              buildDlg = dlgAsm.CreateInstance(dlgType.FullName, true, System.Reflection.BindingFlags.CreateInstance, null, args, null, null) as Form;

              if (buildDlg.ShowDialog() == DialogResult.OK)
              {
                  IBuildDetailSpec querySpec = _serverManager.TfsBuildServer.CreateBuildDetailSpec(
                                                        teamProject,
                                                        _projectName);
                  querySpec.MaxBuildsPerDefinition = 1;
                  querySpec.Status = BuildStatus.InProgress;

                  IBuildQueryResult queryResult = _serverManager.TfsBuildServer.QueryBuilds(querySpec);
                  if (queryResult != null && queryResult.Builds.Length > 0)
                  {
                      _serverManager.UpdateProjectStatus(queryResult.Builds[0]);
                  }
              }
          }
          finally
          {
              if (buildDlg != null)
              {
                  buildDlg.Dispose();
              }
          }
      }
      else
      {
          MessageBox.Show("Unable to locate build dialog");
      }
  }

In Closing

We've covered quite a bit of ground here, and there wasn't really time or space to go into detail about any of the individual pieces, but hopefully you've gotten a flavor of what it takes to both integrate with the TFS server programmatically and create CCTray transport extensions.  I'll have future articles expanding on some of the ideas here as well as some proposed enhancements to the CCTray extension concept and how we might utilize those concepts to provide richer interactions between CCTray and TFS.

Thanks for reading!

Downloads

Implementing a CCTray Transport Extension for Team Foundation Server - Part 1

by TheJet 3. March 2010 19:59

Background

CCTray is a build monitoring application that is part of the CruiseControl.NET suite of build tools.  Originally, when our team began setting up builds within Team Foundation Server 2008 [TFS], we didn't have a build monitoring tool available to us.  A team member had used CCTray on previous projects and put together a web page that we could use to integrate the version 1.4 CCTray tool to monitor the status of our builds using the standard 'red-yellow-green' of continuous integration.  This worked fairly well, but was less than ideal for us, because it required that everyone point their monitoring clients to a server that didn't really belong to our main TFS installation.  So, I embarked upon the task shoehorning native TFS client access into CCTray to monitor the status of TFS builds directly.  This was fairly successful, and is still how the team monitors builds to this day, but unfortunately locks the team into using a fairly old custom build of the CCTray component to do the monitoring.

As I was investigating what it would take to upgrade the system to the new 1.5 version of CCTray, I stumbled upon this blog post by Craig Sutherland, a member of the CruiseControl.NET maintenance team.  In the blog post he described a method by which he added support for extensions to the base CCTray product.  His solution mirrored, in a more generic fashion, the approach I had taken, but had the added benefit of allowing the TFS integration component pieces I had hacked in to be self-contained in an extension DLL and deployed separately from the mainline CCTray executable.  This should allow simpler deployments and easier upgrades to newer versions of the tool for our team.

Sidebar: It should be mentioned at this point that the excellent Team Foundation Server Power Tools does contain a build monitoring application, but we found that it frequently 'lost track' of the builds and would refuse to update its state.  It also didn't give quite the same 'red-green-yellow' experience that we were looking for.

The Investigation

With the extension model in mind, I went out and grabbed the latest 1.5 RC1 release candidate build for CruiseControl.NET.  I was delighted to find that the transport extension pieces that Craig had outlined in his blog post had been [partially] integrated into the build, and proceeded to see what it would take to adapt my TFS integration code into an extension for the 1.5 CCTray client.

The UI for adding an extension based server transport is fairly straightfoward, first the 'Transport Extension' option is selected and the appropriate extension library is selected.  Once selected, you click the 'Configure Extension' button and an extension specific configuration dialog is presented.  Once configured, clicking OK attempts to contact the server through the extension and download a list of available build projects.

Team Foundation Server stores pretty much everything, including build projects, by 'Team Project'.  In CCTray 1.4, I had the user include both the TFS Server name and team project as part of a 'URL' that CCTray would store for server connection.  This was not very user friendly, as the user needed to ensure correct spelling and make sure the right delimiters were included in the URL.  In this new version, I'd like to leverage the configuration dialog to select both the Team Foundation Server and the Team Project that should be associated with a given transport extension instance.

This was much more straightforward than I anticipated, as the Microsoft.TeamFoundation.Client namespace contains a publicly accessible class called RegisteredServers.  For now, the extension will just pull the current list of TFS servers registered for that user.  Getting Team Projects was similarly easy, as the TeamFoundationServer class allows us to retrieve an ICommonStructureService instance through the GetService() method.  The ICommonStructureService interface has a handy ListAllProjects() method which gives easy access to all the team projects that the current user can access on the selected server.

Armed with this knowledge, it was time to tackle the building of the TFS Transport Extension.

Implementing the Transport Extension

The ITransportExtension interface is fairly straightforward, it provides methods to retrieve an ICruiseServerManager that handles interactions with the server, and some other methods for surfacing configuration and providing detail about the extension.  The interface looks like this:

In the previous version of my code, I had already implemented an ICruiseServerManager implementation, so that code came over pretty much 'out of the box' and I was able to focus on getting the server manager hooked into the appropriate server through the CCTray settings area.  This is where I stumbled upon the first problem.  It turns out that the transport extension implementation that's in the 1.5 RC1 codebase [and the current snapshot as well], does not enable you to actually configure the extension because the configuration button isn't hooked up.  Not only that, but the first thing that happens when clicking 'OK' is that an internal variable in the form is accessed which is never initialized, so an exception is immediately thrown.  Strangely enough, the code that I originally downloaded from the SourceForge site seemed to have a mockup of some instantiation code in a paint event, but recent downloads no longer have that code either.  So, if you want to follow along you'll need to download the patch from the above link and apply it to a local copy of the CCTray 1.5 RC1 source code.

Once the button was hooked up, the next task was to determine how [and when], the various properties that are exposed by the extension are assigned relative to the call to Configure(), which is the target for the custom configuration UI dialog.  It turns out, the flow looks something like this:

  1. Instantiate the Transport Extension implementation
  2. Call Configure() on the extension
  3. Construct a BuildServer instance using ITransportExtension.Configuration.Url and ITransportExtension.Settings property

The BuildServer instance created above is then used to assign the Configuration and Settings properties on future instantiations of the transport extension.  In essence, during Configuration, a 'blank' extension is populated with configuration data, then that configuration data is used to populate future instances of the extension for all build projects tied to a given build server.  The only potentially confusing bit is that during configuration, while the ONLY property in the Configuration class that matters is the Url, you MUST still populate the Configuration property during the call to Configure() or the transport extension cannot be used.

Storing Configuration Details

CCTray's ITransportExtension interface provides very simple string-based configuration settings storage, but each transport extension gets only a single string to store the entirety of its configuration.  In order to facilitate more detailed settings storage, it is necessary for the extension developer to decide how to map the single string of configuration data into some other form for consumption by the extension.  In the case of the TFS extension, I chose to store the configuration details in an in-memory class structure.  Currently the class contains only two properties, the Team Foundation Server hostname and the selected Team Project.  Eventually, I could see this being extended to support setting timeouts, pointing to source control, etc, but for now the server hostname and team project are sufficient.

The next task was to figure out how to provide for the serialization and deserialization of the settings data.  While an external configuration file would have been possible, and maybe desirable in certain scenarios, I wanted to avoid the complexity of another configuration file and decided to leverage the built-in XML Serialization capabilities of the .NET Framework to handle the configuration data.  I marked up the configuration class with the appropriate attributes from System.Xml.Serialization and added some static methods to the class to handle the serialization and deserialization duties.  The resulting class looks like this:

    [XmlRoot(ElementName="TFSServerManagerSettings")]
    public class TFSServerManagerSettings
    {
        #region Configuration Properties
        [XmlElement(ElementName="Server")]
        public string ServerName { get; set; }

        [XmlElement(ElementName = "Project")]
        public string TeamProject { get; set; }
        #endregion

        #region Serialization/Deserialization Support
        public static TFSServerManagerSettings GetSettings(string settingsString)
        {
            if (String.IsNullOrEmpty(settingsString))
            {
                return new TFSServerManagerSettings();
            }
            else
            {
                XmlSerializer ser = new XmlSerializer(typeof(TFSServerManagerSettings));
                using (StringReader rdr = new StringReader(settingsString))
                {
                    return ser.Deserialize(rdr) as TFSServerManagerSettings;
                }
            }
        }

        public override string ToString()
        {
            XmlSerializer ser = new XmlSerializer(typeof(TFSServerManagerSettings));
            StringBuilder sb = new StringBuilder();
            using (StringWriter writer = new StringWriter(sb))
            {
                ser.Serialize(writer, this);
            }

            return sb.ToString();
        }
        #endregion
    }

In this case I've chosen to override the ToString() method, whether or not this is the best solution is left up to the reader, but for this example it seemed to be the most 'natural' choice, given that the goal was to turn the class into a string.  Luckily in this case, this particular class is never thrown into list boxes or the like that would naturally call ToString() and expect some other representation than XML.

Next Steps

Now that we've defined our configuration data and have the UI linked together to provide the configuration bits, we can move into the meat of the extension, which is actually communicating with the Team Foundation Server and monitoring the builds.  We'll cover those pieces in Part 2.