Welcome to MSDN Blogs Sign in | Join | Help

What is a stretching treeView?

Recently, I found the need to have a TreeView in my WPF application that was only a few levels deep. I didn't want a horizontal scrollbar to appear and I wanted the long text nodes to wrap. So, I invented the StretchingTreeView control.

Why couldn't I do this with a normal WPF TreeView?

Well, you can, but you have to replace the ControlTemplate for all the TreeViewItems. That's either a lot of XAML or a lot of code.

So, what exactly is the problem?

If you look at the default ControlTemplate for a TreeViewItem, you will see that it contains a grid with 3 columns. In the first column is the expander, in the second column is the Header of the TreeViewItem. And in the last column is nothing. The second column is set to a Width of Auto which means that it will size to its contents. The third column is set to a Width of Star which means it will grow and shrink with the width of the TreeView. No matter what you put in the Header of the TreeViewItem, it will never stretch to the right edge of the TreeView. And Since the Header column is set to Auto, it will never cause a TextBlock or anything else to wrap. Instead, if you turn off the horizontal scrollbar, your long text items simply get clipped at the right edge of the TreeView.

So, what's the code to fix it?

Well it was actually very simple. Once I found out that you can control what kind of Controls a TreeView creates for its items. By subclassing TreeView and overriding the methods GetContainerForItemOverride and IsItemItsOwnContainerOverride, you can control what types are created for your tree. This is especially important if you are databinding your tree to some hierarchy of objects and can't just create TreeViewItems directly. In any case here is the code...

    class StretchingTreeView : TreeView
    {
        protected override DependencyObject GetContainerForItemOverride()
        {
            return new StretchingTreeViewItem();
        }

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is StretchingTreeViewItem;
        }
    }

    class StretchingTreeViewItem : TreeViewItem
    {
        public StretchingTreeViewItem()
        {
            this.Loaded += new RoutedEventHandler(StretchingTreeViewItem_Loaded);
        }

        private void StretchingTreeViewItem_Loaded(object sender, RoutedEventArgs e)
        {
            // The purpose of this code is to stretch the Header Content all the way accross the TreeView. 
            if (this.VisualChildrenCount > 0)
            {
                Grid grid = this.GetVisualChild(0) as Grid;
                if (grid != null && grid.ColumnDefinitions.Count == 3)
                {
                    // Remove the middle column which is set to Auto and let it get replaced with the 
                    // last column that is set to Star.
                    grid.ColumnDefinitions.RemoveAt(1);
                }
            }
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            return new StretchingTreeViewItem();
        }

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is StretchingTreeViewItem;
        }
    }

Basically, all I do is create my own TreeViewItems that wait until they are loaded and then fix the Grid by removing the middle column. I don't like the fix, because it seems like a hack, but it's hardly any code. And that I like.

I hope this helps somebody out there!

In an previous post (long long ago), I described some scenarios around why you would want to subscribe to change the work item tracking subscription to the build completion event. The purpose there was to help users "correct" the subscription that came out of the box. In this post, I would like to answer a question I got from a user about how to subscribe to the other build event - BuildStatusChangeEvent. The question was basically this: How do I get notified when the build quality changes from 'X' to 'Y'?

First, let me say that the name of this event is all wrong. It does not fire when the status of a build changes, but rather when the quality field of the build changes. In the future, it may do more, but not for now. The name aside, it is important to look at the structure of the xml that is sent when the event fires. It is actually very short. Here is an example:

<?xml version="1.0" encoding="utf-16"?><BuildStatusChangeEvent xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <TeamFoundationServerUrl>http://jpricket-test:8080</TeamFoundationServerUrl>
  <TeamProject>TestProj</TeamProject>
  <Title>TestProj Build CSharp_Tests_20080507.3 Quality Changed To Ready for Deployment</Title>
  <Id>CSharp_Tests_20080507.3</Id>
  <Url>http://jpricket-test:8080/Build/Build.aspx?artifactMoniker=9</Url>
  <TimeZone>Eastern Daylight Time</TimeZone>
  <TimeZoneOffset>-04:00:00</TimeZoneOffset>
  <ChangedTime>5/9/2008 3:05:33 PM</ChangedTime>
  <StatusChange>
    <FieldName>Quality</FieldName>
    <OldValue>Lab Test Passed</OldValue>
    <NewValue>Ready for Deployment</NewValue>
  </StatusChange>
  <ChangedBy>NORTHAMERICA\jpricket</ChangedBy>
</BuildStatusChangeEvent>

The important bit is under the StatusChange node which also makes it a little more interesting than the last post that I did. To filter this event in any useful way requires you to know a little more about XPath. Or at least how the "path" to the OldValue and NewValue fields are formed. It's actually pretty simple. Ignore the root node BuildStatusChangeEvent and then use a slash between each node that you need to reference in the path. For example, if you want to filter on the NewValue field, you might want to use the filter expression 'StatusChange/NewValue'='Ready for Deployment'.

Finally, here are some scenarios that you might find yourself in:

  1. I want to be notified when a build is marked as "Released"
  2. I want to be notified when the build quality is removed (only applies to 2008 and above)
  3. I want to be notified when the build changes from "Rejected" to "Released"

And here are the filters that correspond to those scenarios:

  1. 'StatusChange/NewValue'='Released'
  2. 'StatusChange[count(NewValue) = 0]' <> null AND 'StatusChange[count(OldValue) > 0]' <> null
  3. 'StatusChange/OldValue'='Rejected' AND 'StatusChange/NewValue'='Released'

The first and last are exactly what you might expect. But the second filter was quite a challenge. It turns out that when OldValue or NewValue are null, they are not included in the XML at all. So, to see if NewValue was set to null, we have to get the StatusChange nodes that have a count of NewValue nodes equal to zero. If any StatusChange nodes are returned then that half of the filter is true. The other half is just the opposite. We want to make sure that the StatusChange node has at least 1 OldValue field. The syntax of the xPath query is a little hard to get right, but it does exactly what we want.

Hopefully, this will help you with this and other TFS Events! I have attached the code that I used to add these subscriptions to my server. It is very similar to the code in my previous post.

In Team Build 2005, the "definition" of the build was the same as the TfsBuild.proj file. However, in 2008, we changed this so that the TfsBuild.proj only held some of the information for the build. The rest of the information (like retention policies, workspace definition, and triggers) were all stored in the build database. 2008 even allows two different Build Definitions to share the same TfsBuild.proj file.

Unfortunately, due to this diversion of "definition" and source control file (TfsBuild.proj), users have found it difficult to easily get to the TfsBuild.proj file from the UI. Okay, you can find it, but you have to follow this procedure:

1) open the associated definition from team explorer

2) go to the project file tab

3) write down the server path

4) open Source Control Explorer

5) Navigate to the right server path

6) look for the file TfsBuild.proj

Well, in 2008 SP1, you don't have to do all that work any more. Simply right click on the definition in team explorer and choose "View Configuration Folder". This menu item will do all the of the above for you. In future versions, we may even dream of world without TfsBuild.proj files :o

For more information on all the new features in Team Foundation Server 2008 SP1, see bharry's blog about them.

(or Designing Your Workflow Activity Class Hierarchy)

One of the questions that we've had to consider when designing our Workflow Activity Class Hierarchy is whether it is valuable to have our own Activity base class (derived from Activity and inherited by all of our activity classes).

Our answer was "Yes". We decided that there were several good reasons to create our own Activity base class. Here are the reasons and some explanation.

  1. Implement the Asynchronous Activity pattern for all our activities - I described this pattern and the reasons that you want to do this in a previous post. Basically, the idea is that we want all our activities to do their work on a background thread so that they are cancelable.
  2. Shared properties - there were a few properties that we felt like we wanted on all our activities. I won't get into the specifics of them, but this is a reason to create any base class.
  3. Common tracking and logging - we wrote a little bit of code associated with Activity tracking and logging information about the workflow. We wanted to be consistent in the way that we logged about activities starting and stopping, so we put this code into the base class. We even have some generic code that logs all the Properties for our activities using the property collection that is inherited from the actual Activity class.

Our base activity class gives us all this but here's the rub...

There are actually two Workflow Activity base classes. If you want to create a Composite activity (one that contains other activities), you have to derive that class from CompositeActivity instead of Activity. This is extremely unfortunate for us, because there were several activities that we wanted to create that were composite activities. But this means that they couldn't derive from our other base class. So, we had to re-implement some of the base classes functionality in our Composite activity class. This is why when I am designing a tree structure, I try not to make a separate base class for the tree nodes and the leaf nodes. Because, it screws up inheritance, polymorphism and a few other fancy OO words I can't recall.

Keep this gotcha in mind when you start planning your Activity class hierarchy!

In my previous blog post (Creating an Asynchronous Workflow Activity), I explained why your custom activities should either be really fast or run asynchronously. But, I didn't give you a real world example of how to do this. In this post I provide an example of the pattern that my team uses when creating a custom activity.

Pattern:

1) Create a service that actually does the work

2) Create a custom activity that uses the service and has a Queue listener event handler to get a message back from the service when the work is done.

Instead of walking you through this one, I just want to present you with the code, commented as much as I could :)

    public class DoSomethingAsyncService : WorkflowRuntimeService
    {
        private Random m_random;

        public DoSomethingAsyncService()
        {
            m_random = new Random();
        }

        public void DoSomethingAsync(Guid instanceId, Guid queueId)
        {
            ThreadPool.QueueUserWorkItem(delegate(Object state)
                {
                    // Fake a call to a WebService (let's say it takes 10 seconds)
                    Console.WriteLine("Making webservice call.");
                    Thread.Sleep(10000);

                    // Create the QueueItem
                    QueueItem item = new QueueItem();
                    item.ReturnCode = m_random.Next(0, 10);
                    item.Message = String.Format("The return code is {0}.", item.ReturnCode);

                    try
                    {
                        // Now send this item to the appropriate queue
                        WorkflowInstance instance = Runtime.GetWorkflow(instanceId);
                        instance.EnqueueItem(queueId, item, null, null);
                    }
                    catch (InvalidOperationException)
                    {
                        // Catch any InvalidOperationExceptions that occur due to the 
                        // workflow having already completed, etc.
                    }
                });
        }
    }

    public class QueueItem
    {
        public int ReturnCode;
        public String Message;
    }
    public class DoSomethingAsyncActivity : Activity
    {
        private Guid m_queueId;

        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
            // Create my queue
            m_queueId = Guid.NewGuid();
            WorkflowQueuingService queuingService = (WorkflowQueuingService)executionContext.GetService(typeof(WorkflowQueuingService));
            WorkflowQueue queue = queuingService.CreateWorkflowQueue(m_queueId, true);

            // Hook the item available event. This event will be triggered when our
            // service enqueues an item.
            queue.QueueItemAvailable += new EventHandler<QueueEventArgs>(QueueItemAvailable);

            // Call the service to perform the async work.
            DoSomethingAsyncService service = (DoSomethingAsyncService)executionContext.GetService(typeof(DoSomethingAsyncService));
            service.DoSomethingAsync(WorkflowInstanceId, m_queueId);

            // Return Executing to let the workflow know that we are still working.
            return ActivityExecutionStatus.Executing;
        }

        private void QueueItemAvailable(object sender, QueueEventArgs e)
        {
            // The activity execution context is always passed in as the sender.
            ActivityExecutionContext context = sender as ActivityExecutionContext;

            // Checking for a null context just in case something really strange is happening.
            if (context != null)
            {
                // Get the queue service so we can get the Queue object.
                // Note: you cannot salt away the actual queue object, the Workflow runtime does not allow it.
                WorkflowQueuingService queuingService = context.GetService<WorkflowQueuingService>();

                // Make sure the queue exists before going any further.
                if (queuingService.Exists(m_queueId))
                {
                    WorkflowQueue queue = queuingService.GetWorkflowQueue(m_queueId);

                    // Dequeue the item that was queued by the service.
                    QueueItem item = queue.Dequeue() as QueueItem;
                    if (item != null)
                    {
                        // We only expect one thing to ever be put into our Queue;
                        // so, now we remove the queue.
                        queue.QueueItemAvailable -= QueueItemAvailable;
                        queuingService.DeleteWorkflowQueue(m_queueId);

                        // Finally inspect the results and do something with them.
                        Console.WriteLine(item.Message);

                        // Once we have finished our mission, we close this activity
                        context.CloseActivity();
                        return;
                    }
                }
            }

            // We should not get here if everything works as we expect.
            // So, throw an exception so the workflow will be terminated.
            throw new Exception("Something unexpected was placed in the Queue.");
        }
    }

Try creating a workflow that contains a Parallel activity and add this activity to both branches. If you really want to run activities in parallel, this is how it needs to be done.

Don't forget to add the service to the runtime!

Happy flowing!

What a silly topic for a blog post! Workflow already runs my whole activity tree on a another thread, I don't have to worry about being Async, Do I? Yes, you do. While it is true that each Workflow gets its own thread (in the default setup), that thread can be easily blocked by a long running or hung activity. Let's look at an example...

Create a Sequential Workflow Console application in Visual Studio with the following Activity tree:

Sequential Workflow

    WhileActivity (set the condition to true so it runs forever)

        CodeActivity (the code should just do a Console.WriteLine("Hello World")

So, now go to the Program.cs file or wherever the code is that Starts the workflow instance.

Add a Thead.Sleep(1000) after the instance.Start() call and then an instance.Terminate("kill workflow") to stop the workflow.

When you run the workflow, you should notice that you get a few "Hello World" lines and one "kill workflow" line. Here, terminate works just as you expect it to. But now let's change our workflow so that the while is done in the code activity...

Sequential Workflow

    CodeActivity (the code should contain while(true) Console.WriteLine("Hello World"))

When you run the workflow this time, it will never end :(

If you put a break point on the instance.Terminate line, it does get called. So, why doesn't the Workflow terminate? Well, like all other actions that happen in the workflow instance, the terminate action goes through a queue and waits for the background thread to dequeue it and do it. Unfortunately, the queue is only processed in between activities executing. So, the termination request remains in the workflow queue forever in our example.

If you are running complex workflows that may need to actually be stopped in the middle (like we do) then you need to avoid code activities that have loops and start writing asynchronous custom activities. So, how do we write a custom activity so that it is stoppable? The first thing you have to do is create a workflow runtime service that can do the actual work for the activity. This means for every custom activity you need a new custom service or at least a new method on your custom service. Here's a sample service:

    public class HelloWorldService : WorkflowRuntimeService
    {
        public void WriteHelloWorldForeverAsync()
        {
            ThreadPool.QueueUserWorkItem(delegate(Object state)
                {
                    while (true) Console.WriteLine("Hello World");
                });
        }
    }

This service will now do the work for us. But to use it, we need a custom activity. Here's the simplest custom activity you could make:

    public class WriteHelloWorldActivity : Activity
    {
        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
            HelloWorldService service = (HelloWorldService)executionContext.GetService(typeof(HelloWorldService));
            service.WriteHelloWorldForeverAsync();
            return ActivityExecutionStatus.Executing;
        }
    }

The key thing to note here is that the activity calls a method on the service that returns immediately after starting its background thread. The activity then returns Executing. This allows the workflow to dequeue items from the queue, but not move on to the next activity. Until this activity returns Closed, the next activity will not start.

Oops! before you try and run this you need to create your service, add it to the runtime and add your custom activity to the workflow. In Program.cs, add the following line right after you create the runtime service:

workflowRuntime.AddService(new HelloWorldService());

Then change the workflow to look like this:

Sequential Workflow

    WriteHelloWorldActivity

Now, you can run it and see that Terminate works again! Of course, in a real application you need more code than this, because you have to shut down the background thread when you get terminated, and you need to call back to your activity when the background work is finished! Tune in for my next post, where I will show you a real example of of an activity and service that do it all :)

In Visual Studio Team System 2008, the build machine downloads the files that it needs to launch MSBuild before it creates any workspaces. Because this initial "get" is before MSBuild is called, users don't get much say in what gets downloaded. By default, the build machine downloads all the files in the ConfigurationFolderPath of the build definition that is being built. You specify that path in the Build Definition dialog. Everthing at the root of that folder is then downloaded before the build starts.

The problem that some people have run into is that we only download the root of the folder. They have some assemblies or other specific build files that are nessesary in subfolders of that location. Luckily we thought of that. You can change the download parameters so that all subdirectories are downloaded as well. Be careful, however, if you specify to download everything, you will get everything under that folder that exists in version control. You cannot cloak anything out. Also note that this "fix" is per build machine, not build, and not build definition.

Here are the steps:

  1. Log into the build machine that you want to change (you probably need to be an admin on the box).
  2. Locate the TFSBuildService.exe.config file (most likely in "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies")
  3. open it in notepad or some other text editor
  4. Near the bottom (before the /appsettings closing tag) add the following line:
    • <add key="ConfigurationFolderRecursionType" value="Full" />
    • other values include OneLevel or None. OneLevel is the default.
  5. Save the file
  6. Restart the "Visual Studio Team Foundation Build Service" from the Services control panel applet.

That's it. I hope this helps!

In VS2008, you still have to run the Wizard to create a TfsBuild.proj file for your build definition. That wizard can be really slow to open. In fact, internally the wizard can seem to hang indefinitely. And if you have a large number of files checked into your Team Project you may experience the same thing. But, there is something you can do.

The Problem:

When the wizard opens it queries the version control server for all SLN and VSMDI files in the Team Project. The larger the Team Project the longer this takes.

The Solution:

In VS2008 we limited this exhaustive search to the workspace that is defined in the build definition. So, if you limit the number of files in the workspace, the query will run much faster.

Problem #2:

Because the definition dialog is where you define the workspace and launch the wizard, we don't force you to do one before the other. And the default workspace is the entire team project. That means that if a user tries to run the wizard first, they will run into the exact same slowness that was there in Version 1.

There is not a solution for this second problem except user training. Hopefully, you don't have too many people that are creating build definitions.

Happy Building!

Another one of the newest technologies released by Microsoft is WPF (Windows Presentation Foundation). This is a completely new way of thinking about User Interfaces. One of the things that I find the most interesting is the way that complex controls are created in WPF. Let's take a quick look at the TreeView control in WPF.

If you are used to the Windows Forms TreeView, you should probably make a point to forget everything that you know about it. The WPF TreeView is really a composite of other elements (it is not a wrapper around the Win32 TreeView - for better or worse - I think better!). To get the look that you want for a control, WPF provides a Style for the control that is made up of the other simpler elements that give it the appropriate look. The TreeView is visually made up of a Border, then a ScrollViewer, and then an ItemsPresenter. So, I really haven't investigated what an ItemsPresenter is yet, but you should get the idea... the "control" is made up of other elements that deal with the drawing and the control is left to deal with the behavior.

If you want to change the look of a TreeView, just change the style. You can change the border, remove the scroll viewer, or add any other elements you like.

But the real reason I wanted to write this post was to share the code that I had to write to programmatically scroll the treeview. Why would I do that? Well, I was implementing Drag n Drop on this particular TreeView and needed to scroll for the user when they were dragging near the bottom or top of the tree. Because you can change the Style of the TreeView, there isn't a built in way to get to the scrollviewer which really stinks. But, knowing that my control wouldn't change, I was able to create a simple property on my subclasses TreeView to get the scroll viewer and here it is:

        private ScrollViewer ScrollViewer
        {
            get
            {
                if (m_scrollViewer == null)
                {
                    DependencyObject border = VisualTreeHelper.GetChild(this, 0);
                    if (border != null)
                    {
                        m_scrollViewer = VisualTreeHelper.GetChild(border, 0) as ScrollViewer;
                    }
                }

                return m_scrollViewer;
            }
        }

As you can see, if you know your Visual tree, it is easy to get the ScrollViewer. Once I had the scrollviewer the logic to scroll the tree view was simple as well:

        internal void ScrollIfNeeded(Point mouseLocation)
        {
            if (this.ScrollViewer != null)
            {
                double scrollOffset = 0.0;

                // See if we need to scroll down
                if (this.ScrollViewer.ViewportHeight - mouseLocation.Y < 20.0)
                {
                    scrollOffset = 3.0;
                }
                else if (mouseLocation.Y < 20.0)
                {
                    scrollOffset = -3.0;
                }

                // Scroll the tree down or up
                if (scrollOffset != 0.0)
                {
                    scrollOffset += this.ScrollViewer.VerticalOffset;

                    if (scrollOffset < 0.0)
                    {
                        scrollOffset = 0.0;
                    }
                    else if (scrollOffset > this.ScrollViewer.ScrollableHeight)
                    {
                        scrollOffset = this.ScrollViewer.ScrollableHeight;
                    }

                    this.ScrollViewer.ScrollToVerticalOffset(scrollOffset);
                }
            }
        }

I won't go into a lot of explanation, but if the user is dragging the mouse 20 units (not pixels) from the top of the scroll area or 20 units from the bottom of the scroll area, I simple move the scroll area the appropriate direction by 3 units.

I hope this post helps someone out there trying to do the same sort of thing!

This post continues my quest to learn more about Windows Workflow and share that experience with you. It assumes knowledge of Windows Workflow. If you don't understand some of the concepts please take a look at one of the books that I mentioned in my first WF post.

One of the operations that we require is the ability to Stop a Build that is in progress. This corresponds to Terminating a workflow. However, in our investigation of how WF handles Terminate, we discovered that it isn't what we really want. Termination of a workflow does not end the workflow immediately. That's not so bad, but it also doesn't give the workflow a chance to clean up anything (i.e. there are no handlers for termination). So, we looked into Cancellation of a workflow instead of using terminate.

We found that it is really hard to cancel a workflow. First of all there is not a WorkflowInstance.Cancel method. So, if you plan to cancel the workflow from outside the instance, you have a hurdle to jump already. But if your workflow is like ours and you can put your own Activity class at the root, you can leap the hurdle pretty easily. What we did was to have the root activity create a Queue (we called it the cancellation queue) with a well known name (a hard coded Guid). From there it seemed obvious that we could Queue an item to this well known Queue and from within the instance call CancelActivity on the root. But alas, that is not allowed. You can only call CancelActivity on the currently executing activity. Bummer! So, we tried cancelling the executing activity, but that ended up just continuing the execution of the next activity after it which wasn't cancelled. Double Bummer!

So, our final solution was to Throw when we got something in the Queue. Throwing automatically causes all activities to cancel including the active one. It also gives the workflow a place to do any clean up that needs to be done by catching the CancellationException at the top of the workflow.

It wasn't pretty, but we were able to accomplish what we wanted. Here is a summary of the steps that actually worked for us:

  1. In the root level activity (which is our own class derived from one of the WF activities) we create a special Queue just for this situation. The Queue has a constant GUID for the name
  2. From outside the workflow, we get the WorkflowInstance and Enqueue an item into this special Queue.
  3. When an item is enqueued, our activity class Throws a special exception. That causes everything to get cancelled.
  4. Our cancellation handlers can do any clean up.
  5. We have a special fault handler that also catches this special exception and eats it, so that the workflow completes normally.

In the US, most states have a time period from about March to November where we actually change our Time Zone. Normally the Eastern part of the United States is in the Eastern Standard Time zone which is -5 hours from GMT. However, during the Spring and Summer, we switch to Eastern Daylight Savings Time which is -4 hours from GMT. This switch takes place on a Saturday night at 2:00 AM.

Because of this switch in Time Zone, a scheduled build may not run when you expect it to. Let me explain how we tried to accommodate for this. The scheduled time is stored in the database in the Server's local time but the server always returns the time in GMT (or Universal Time). We store the time as local time on the server so that when the Time Zone is changed on the server you get back the right time. I better show you an example:

You set up your build to run every night at 1:00 AM. You originally set it up in January from a client on the east coast (that's EST). Your server that is also on the east coast gets the value in GMT from the client which turns out to be 6:00 AM GMT. In March the Time Zone for the Eastern United States switched from EST to EDS (daylight savings time). Now in April when are your builds running? 6:00 AM GMT is actually 2:00 AM in EDS. But thankfully the time was not stored in GMT in the database. Since the time was stored as 1:00 AM local time, it is still 1:00 AM local time. When the scheduler requests the time from the server the time is returned as 5:00 AM GMT which is 1:00 AM EDS. And of course when EDS ends and we are back in EST, the same trick keeps our builds running at 1:00 AM.

If you really paid close attention to that example and got everything I was trying to convey, you probably noticed the Gotcha! As long as your server and clients are in the same timezone, you should never see any problems. As you change the time zones between clients and the server, you may notice some interesting side affects. For instance, if your server is in the pacific time zone, then it doesn't change to daylight savings time until 3 hours later than you. So, you might have one or two builds a year that don't start when you expect them to. And if your server is in a time zone that doesn't switch to daylight savings time at all, but you are in a timezone that does, your builds will run an hour later in the Spring and Summer.

Enjoy Building!

[UPDATE]

Unfortunately, when DST ended in November, we discovered a bug in the Visual Studio Build UI. When it converts from the UTC time on the server to the local time it does it as if it was the first day of the week (that's Sunday). If your build is scheduled to run between 12:00 am and 2:00am, the UI will show it one hour earlier the week that DST ends and one hour later the week that DST starts. The builds will still start on time, but the UI shows it incorrectly. After that one week everything is good, but don't try to change the scheduled time during that week either. It won't get the time you expect because of the messed up conversion. Sorry for the bug, its all my fault :(

[UPDATE 3/19/08]

This Bug is fixed by service pack 1 of VSTS 2008. The service pack should be released before we have another DST change in the Fall.

Team Build is a build workflow tool. It manages how and when builds are performed. Currently (in VS2005 and VS2008), that workflow was written in the language of MSBuild. This allows us a lot of flexibility and power on the machine that runs the build. However, modifying the MS.TF.Build.Targets file that contained the description of the workflow was heavily discouraged and by no means easy. So, how can we make changing the workflow of the build easier. The first step (we believe) is to change the language that the workflow is described in. Windows Workflow is a natural choice. As we look at using Windows Workflow within Team Build, I will attempt to write some useful posts on things that we have discovered or realized. Mostly things that the books don't talk about. This is my first attempt...

First let me tell you about the books that I have read on WF. I read Bruce Bukovics' "Pro WF" first. This is a great starter for a developer wanting to write WF solutions. Bruce covers all of the functionality you would need to know. But, if you want to host WF and allow your users to manipulate the workflow directly, you need more details about how it works. The second and last book that I read on WF was "Essential Windows Workflow Foundation" by Dharma Shukla and Bob Schmidt. The Essentials book is really brief, but explains how WF Activities really work. If you plan on writing your own custom activities, this book is very useful.

So, what did I learn. More importantly, what have I learned that I didn't get from the books. Well the first thing you should know is that you can represent a workflow in many different ways. The workflow is basically a tree of classes derived from Activity. There are two ways to represent the tree in the Visual Studio world: in code; and in XAML (basically XML). Workflow uses the extension XOML, however, to let VS know that its a workflow and not some kind of WPF stuff (more on WPF in another post). Working with code is the most natural way to deal with the Activity tree. In fact, even if you choose to create your workflow in XOML, VS will create a code behind file for you and compile your XOML into a dll.

The first lesson we learned was... If you want to use XOML only and not do any code behind, you must create the XOML yourself or hand edit the XOML created by Visual Studio. When Visual Studio creates the XOML file, it assumes you will compile it. To compile XOML you need a special attribute on the root element in the XOML that tells the compiler to create a class. The attribute is "x:Class". If you create a XOML workflow in Visual Studio and open that XOML in the XML editor or in notepad, this should be the first attribute of the root element.

When you load a workflow in the workflow runtime, there are two ways to do it. Call load with a type, this implies that the XOML or straight code workflow is compiled. Or you can call load with an xml reader, that has the XOML loaded. If you choose the xml reader option and that x:Class attribute is in the XOML, an error will be thrown by the runtime. However, if you try to compile XOML without that attribute, compilation will fail. So, the first choice you have to make in using XOML files is "To compile or not to compile" - that is the question ;)

There have been at least a couple of forum posts from users that would like to run more than one build agent on the same physical machine. The normal reason is that they have more than one TFS server and want to use the machine as a build agent for both.

In this post I will describe how to add another build agent to a machine that already has one installed. The steps are very manual but should be simple enough.

  1. First you need to copy the actual binary for the service and it's config file. The config file is keyed off the name of the binary, and we have to have a separate config file for each.
    • cd "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies"
    • copy TfsBuildService.exe TfsBuildService-2.exe
    • copy TfsBuildService.exe.config TFSBuildService-2.exe.config
  2. Next you need to edit the new config file to point to the other server and use a different port number.
    • notepad TFSBuildService-2.exe.config
    • Search for port and change the port number from 9191 to something new like 8181
    • There's also another port number for interactive running that defaults to 9192, you should change that one as well. Let's say to 8182.
    • Now, search for AllowedTeamServer and set the value to the full URL of the new server.
    • Save your changes.
  3. Now, you can install the new service using sc.exe and get it started.Take note of the spaces after the equals signs in the sc command line.
    • sc \\jpricket-test create "MyVSTFBuild" binPath= "C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies\TfsBuildService-2.exe" DisplayName= "My Team Build Service"
    • Now, open up Control Panel\Administrative Tools\Services.
    • Find "My Team Build Service". Right click on it and open Properties.
    • On the Log On tab, set it to the service account and password that you want to use.
    • Hit OK to save your changes and then start the service. 
  4. Finally, you have to create a build agent on the other server that points to this new machine and port.
    • Open Visual Studio
    • Connect the other TFS server and the correct Team Project
    • Right click on the Builds node in team explorer and select Manage Build Agents.
    • Make sure to create one with the correct port number and machine name.
    • You will also want to change the "Working Directory" to something different that the other instance of the build service uses. I used "$(Temp)\my\$(BuildDefinitionPath)"
  5.  Now, you're ready to build on both agents using the same machine.

UPDATE: I neglected to mention that you should reserve the new port number that you decide to use in step 2. You can read up on how to do this with the wcfHttpConfig tool here. Basically you just need to run a command prompt, change to the privateassemblies area of the Visual Studio program directory (usually C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\PrivateAssemblies) and type the following at the command line:  wcfhttpconfig.exe reserve DOMAIN\TeamBuildAccount 8181

 

In my last post about creating a Fake build, I gave you the code to create a fake build in V1 and I promised to give you the code to do the same thing in Orcas. So, here it is. This code won't work in Beta1 or Beta2, but should work as is when you get an RC or RTM version of Orcas. The CreateManualBuild API was added to the code base post Beta2 cutoff.

So, here's the code:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.TeamFoundation.Build.Proxy;
using Microsoft.TeamFoundation.Client;
using Common = Microsoft.TeamFoundation.Build.Common;
using Microsoft.TeamFoundation.Build.Client;

namespace AddFakeBuild
{
    class Program
    {
        static void Main(string[] args)
        {
            AddBuild("http://jpricket-test:8080", "jpricket-070507", "fakebuild23");
        }

        static void AddBuild(String serverName, String teamProject, String buildNumber)
        {
            // Get the TeamFoundation Server
            TeamFoundationServer tfs = new TeamFoundationServer(serverName);

            // Get the Build Server
            IBuildServer buildServer = (IBuildServer)tfs.GetService(typeof(IBuildServer));

            // Create a fake definition (and agent)
            IBuildDefinition definition = AddDefinition(buildServer, teamProject, "FakeDefinition1");

            // Create the build detail object
            IBuildDetail buildDetail = definition.CreateManualBuild(buildNumber);

            // Create platform/flavor information against which test 
            // results can be published
            IConfigurationSummary confSummary = InformationNodeConverters.AddConfigurationSummary(buildDetail, "Debug", "x86", "");
            ICompilationSummary compSummary = confSummary.AddCompilationSummary();
            compSummary.ProjectFile = "Dummy.sln";
            compSummary.Save();

            // Complete the build by setting the status to succeeded and setting the drop location.
            buildDetail.Status = BuildStatus.Succeeded;
            // The drop location is not copied properly from the definition so we have to copy it manually.
            buildDetail.DropLocation = definition.DefaultDropLocation;
            buildDetail.Save();
        }

        private static IBuildDefinition AddDefinition(IBuildServer buildServer, string teamProject, string definitionName)
        {
            try
            {
                // See if it already exists, if so return it
                return buildServer.GetBuildDefinition(teamProject, definitionName);
            }
            catch (BuildDefinitionNotFoundException)
            {
                // no definition was found so continue on and try to create one
            }

            IBuildAgent agent = AddAgent(buildServer, teamProject, "FakeAgent1");

            IBuildDefinition definition = buildServer.CreateBuildDefinition(teamProject);
            definition.Name = definitionName;
            definition.ConfigurationFolderPath = "$/";
            definition.ContinuousIntegrationType = ContinuousIntegrationType.None;
            definition.DefaultBuildAgent = agent;
            definition.DefaultDropLocation = @"\\MySharedMachine\drops\";
            definition.Description = "Fake build definition used to create fake builds.";
            definition.Enabled = false;
            definition.Workspace.AddMapping("$/", "c:\\fake", WorkspaceMappingType.Map);
            definition.Save();

            return definition;
        }

        private static IBuildAgent AddAgent(IBuildServer buildServer, String teamProject, String agentName)
        {
            IBuildAgent agent = buildServer.CreateBuildAgent(teamProject);
            agent.Name = agentName;
            agent.BuildDirectory = "c:\\nobuilddir";
            agent.Description = "Fake build agent used to create fake builds.";
            agent.MachineName = "NoBuildMachine";
            agent.Port = 9191;
            agent.Status = AgentStatus.Disabled;
            agent.Save();

            return agent;
        }
    }
}

There are a couple of things to take note of here. First, you may have noticed that I am creating a Build Definition and a Build Agent. In Orcas, if you try to create a Build without these things existing, you will get exceptions thrown from the server. However, if you were to use existing Definitions and Agents, the code gets a lot smaller. The next thing you should look closely at is the call to definition.CreateManualBuild. This new method encapsulates most of the code that we did in the V1 version.

CreateManualBuild is located off the IBuildDefinition interface. This allows you to get all the definition defaults for free. There are several overloads for this call. The shortest is used here. It takes in a build number and defaults all the other values. If you really had a manual build that you had copied to a drop location, you could specify the drop location as the next parameter to this call. The long version allows you to specify buildNumber, dropLocation, buildStatus, agent, and requestedFor.

In the above example code, the status of the build after calling CreateManualBuild is InProgress. I then add some project details to the build information and change the status to Succeeded. By setting the Build Status to a completed status myself, the BuildCompletion event is fired as if this was a real build. If the status was set to Succeeded in the CreateManualBuild call, the event would never be fired for this build. This gives you the ability to create historical builds (no events needed) or to create manual builds where you might want the events.

I know many customers will find this new API very useful.

Why would anyone want to add a fake a build to the team build server? Well, there is one very big reason - Integration with Team System. If you don't use Team Build V1 to build your sources (for whatever reason), you may want to at least store some build information in the Team Build server. This allows you to publish test results, associate work items with builds, and view your build information from within Visual Studio.

I saw an older post by someone else that had most of the code. Unfortunately, they were missing the line at the bottom that actually Completes the build. Without this line you won't be able to associate work items with a build number. The code is commented and should be very straight forward. Note that in V1 this process requires several Web Service calls not just one.

So, here is the code:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.TeamFoundation.Build.Proxy;
using Microsoft.TeamFoundation.Client;
using Common = Microsoft.TeamFoundation.Build.Common;

namespace AddFakeBuild
{
    class Program
    {
        static void Main(string[] args)
        {
            AddBuild("http://TeamBuildRTMSP1:8080", "MsfAgile", "FakeBuild001");
        }

        static void AddBuild(String serverName, String teamProject, String buildNumber)
        {
            // Get the TeamFoundation Server
            TeamFoundationServer tfs = new TeamFoundationServer(serverName);

            //Construct build store and build controller objects
            BuildStore bStore = (BuildStore)tfs.GetService(typeof(BuildStore));
            BuildController bController = (BuildController)tfs.GetService(typeof(BuildController));

            //Create a build entry
            BuildData bd = new BuildData();

            //Fill in mandatory information for BuildData object
            bd.BuildNumber = buildNumber;
            bd.BuildType = "DummyBuildType";
            bd.TeamProject = teamProject;

            // Make sure that this drop location exists, otherwise 
            // test publish will fail
            bd.DropLocation = @"\\MySharedMachine\drops\" + buildNumber;
            bd.BuildMachine = "NoBuildMachine";
            bd.RequestedBy = Environment.UserName;

            // These string values are locale dependent in V1
            bd.BuildStatus = Common.BuildConstants.BuildStatus.BuildSucceeded;
            bd.BuildQuality = "Not Examined";

            // Add build entry to TeamBuild DB
            bStore.AddBuild(teamProject, bd);

            // Get the URI of the build
            string buildUri = bStore.GetBuildUri(teamProject, buildNumber);

            // Create platform/flavor information against which the test 
            // results will be published
            ProjectData pd = new ProjectData();

            // Fill in mandatory information for ProjectData object
            pd.FlavourName = "Debug";
            pd.PlatformName = "x86";
            pd.ProjectFile = "Dummy.sln";

            // Add project data entry for this build.
            bStore.AddProjectDetailsForBuild(buildUri, pd);

            // Fill in the finish time
            bStore.UpdateBuildFinishTime(buildUri, DateTime.Now);

            // Complete the build and fire the BuildCompletion Event
            bController.BuildCompleted(buildUri);
        }
    }
}

In my next post, I will show you how to do this in Orcas!

More Posts Next page »
 
Page view tracker