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

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 :)

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.