How do you know when a text box has an auto-complete?

I was working with TFS Build when I ran into this screen:

image

I went to Windows Explorer to get the entire path to CTRL-C/V into the text box when I realized that the text box had an auto-complete feature.  This saved me the time to switch between Windows Explorer and TFS, which was good.  However, outside of the documentation, how would I know that the text box has auto-complete turned on?  Drop Down text boxes have a down arrow by convention, multi-lists have a vertical bar, but there is nothing for auto-complete.  When Google introduced auto-complete, it expected a search phrase so they did not include any visual clue:

image

I am wondering if there should be some kind of visual clue that the text box auto-completes?  Perhaps a red-arrow in the far-right of the box? Anyone have any ideas?

Advertisements

Recursive WF Activities

I want to start backing up my home machine hard drive. There are a bunch of services out there and some of them use size-based storage – the more space you use, the more they charge you. When you look in Windows Explorer, there is not an easy way to determine the size of a given directory – just file sizes:

clip_image001

Since I am not backing up the entire drive, I can’t look at the drive size and space used to determine how much a storage company will cost – so I can’t adequately price compare.

clip_image002

I then thought of whipping up a quick recursive function to get the cumulative size of all of the files in a directory. I then thought I could use the exercise to investigate recursion in WF.

I set up a test location on my file system (I know, I should be using a Mocking Framework):

clip_image004

I then wrote a function that gets the total size of all of the files in a directory (and any subdirectory) like this:

 

static long GetFileSize(string directoryName) { long cumulativeSize = 0; foreach (FileInfo fileInfo in new DirectoryInfo(directoryName).GetFiles()) { cumulativeSize += fileInfo.Length; } return cumulativeSize; }

I then wrote a unit (integration, really) test to verify the function as it is currently written:

[TestMethod()] [DeploymentItem("Tff.DirectorySize.exe")] public void GetFileSizeTest() { string directoryName = @"C:\TestDirectory"; long expected = 12647; long actual = Program_Accessor.GetFileSize(directoryName); Assert.AreEqual(expected, actual); }

I then added the recursion bit into the function:

static long GetFileSize(string directoryName) { long cumulativeSize = 0; DirectoryInfo directoryInfo = new DirectoryInfo(directoryName); foreach (FileInfo fileInfo in directoryInfo.GetFiles()) { cumulativeSize += fileInfo.Length; } foreach (DirectoryInfo subdirectoryInfo in directoryInfo.GetDirectories()) { cumulativeSize += GetFileSize(subdirectoryInfo.FullName); } return cumulativeSize; }

Still got green on my original unit test. I then added a new subdirectory to the test directory and copy/pasted the original test files into the test subdirectory. I changed the unit test to be like so:

[TestMethod()] [DeploymentItem("Tff.DirectorySize.exe")] public void GetFileSizeTest() { string directoryName = @"C:\TestDirectory"; long expected = 12647 * 2; long actual = Program_Accessor.GetFileSize(directoryName); Assert.AreEqual(expected, actual); }

I still got green. This is a great exercise to show the problems with mixing integration tests with unit tests and the usefulness of Mocking frameworks.

I then added a WF Activity to the project. Interestingly, I could not add a variable to this blank activity.

I added a Flowchart activity to the project and added a local variable called cumulativeSize. A pain-in-the-butt feature of WF is that long (and most other types) and not available in the select box.

clip_image006

When browsing, remember to look in MSCorLib and System Namespace

clip_image007

If you forgot that long is actually System.Int64

clip_image008, In

Intellisense can help you:

clip_image010

In any event, the 1st line of the function was replicated in WF:

clip_image012

I then realized I need to set up the input parameters of the function/flowchart. Easy Enough:

clip_image014

I then looked at the next line of the function:

I need a way of representing an instantiated DirectoryInfo object in my WF using that input argument. I realized that I need to wrap the DirectoryInfo class with an activity to use it. I dropped in a code activity thinking that all I had to do was implement the IDirecotryInfo interface. Note that to avoid name collisions, I called the activity “DirectoryInfoActivity”, not “DirectoryInfo”.

Alas, there is no such thing as a IDirectoryInfo interface. DirectoryInfo inherits from FileSystemInfo and is a sealed class. Ugh! If it was at least a partial class, I could reverse-engineer the interface (like I do using EF4), but that is out.

I then realized that I should just make the DirectoryInfo class a local variable for the workflow:

clip_image016

I could then do a For..Each activity on that local variable. A couple of key points:

You need to set the property of the ForEach Type to System.IO.FileInfo like so:

clip_image018

Just changing it in the Designer <FileInfo> doesn’t do anything.

Also, working in VB syntax in little text boxes can be very frustrating. Using the elipises in the property window is very helpful:

clip_image020

Next, I created a Unit Test (manually, a context menu with Create..UnitTest would be very helpful Microsoft). I had to lookup that the WorkflowInvoker is in System.Activities:

[TestMethod] public void GetFileSizeTest() { string directoryName = @"C:\TestDirectory"; long expected = 12647; GetFileSize getFileSize = new GetFileSize(); getFileSize.directoryName = directoryName; IDictionary<string, object> results = WorkflowInvoker.Invoke(getFileSize); long actual = Int64.Parse(results["cumulativeSize"].ToString()); Assert.AreEqual(expected, actual); }

The first time I ran it, I got a null reference exception -> apparently you have to New up your objects – WF doesn’t do that for you:

clip_image022

And then we get a pass:

clip_image023

The next challenge is to add in the recusion. I put in the next For..Each Loop:

clip_image024

clip_image025

But the problem is that the right-hand side needs to invoke the workflow again. I don’t see any InvokeWorkflow activies in the toolbox, so I made a code activity. This post is taking longer than I want, so I’ll drop in the remaining tomorrow.

FTP and WF in TFS

Following the creation of my FTP WF tasks that I created here, I then went to add those tasks into the TFS Workflow.  I already have created a special template and found where I want to add the tasks here so it was only a matter of dropping the tasks into the WF, right?  Wrong.  The WF templates are not part of any given project, so you can’t just create WF tasks in a project and reference them in the template.  You need to add them to the toolbar and to do that, you need to create the tasks in an independent project.  I first tried to right click on the toolbox and add a reference to the project that I just created, however when I went to drag…drop the task into the designer, I got the red “no” symbol.  After some research, I found this post – I have to add these activities to the GAC.  After strong naming the original project and using GACUtil, I then could navigate to C:\Windows\Microsoft.NET\assembly\GAC_MSIL (not C:\Windows\assembly btw) and bring the FTP tasks in.  Interestingly, the top-level task (the sequence) did not “bubble up” the child in and out parameters so I had to drag the 3 actual code activities from the designer and place them into the TFS WF.

image

Once I did that, I need to hook up the build directory to the source for the FTP Copy Directory task.  I didn’t try to alter the path at all, so I had to use the fully-qualified path as so:

image

This corresponds to the file system like this:

image

Note that I am only copying the website (the same way VS2010 does it) and I am not copying the source code or the debugging symbols.

After completing that task, I then checked the project into source control (Development to Main to Release). The build kicked off and the files moved up to the host as expected:

image

Cow-A-Bunga!

I would like to then refactor the WF activities to take all of the code behind and put them on the designer, but that is next week – once I implement these FTP activities to all of the websites that I maintain.

FTP and WF: Setting the Stage

Following up on my post here regarding TFS and WF, I went to write the actual WF tasks that correspond to the FTP activities of moving files over to a remote host.  I crated 3 tasks

The first connects to the FTP Host:

public sealed class ConnectToHost : CodeActivity { FTPConnection _ftpConnnection = null; public InArgument<string> HostName { get; set; } public InArgument<string> UserName { get; set; } public InArgument<string> Password { get; set; } public InOutArgument<FTPConnection> FTPConnection { get; set; } public ConnectToHost() { _ftpConnnection = new FTPConnection(); } public FTPConnection ActiveConnection { get { return _ftpConnnection; } } protected override void Execute(CodeActivityContext context) { _ftpConnnection.ServerAddress = HostName.Get(context); _ftpConnnection.UserName = UserName.Get(context); _ftpConnnection.Password = Password.Get(context); _ftpConnnection.Connect(); } }

I then created a Disconnect Task:

public sealed class DisconnectFromHost : CodeActivity { FTPConnection ftpConnection = null; public DisconnectFromHost() { } public DisconnectFromHost(FTPConnection ftpConnection) { this.ftpConnection = ftpConnection; } protected override void Execute(CodeActivityContext context) { ftpConnection.Close(); } }

And finally created the Task that does the actual copying:

public sealed class CopyDirectoryContents : CodeActivity { FTPConnection ftpConnection = null; public CopyDirectoryContents() { } public CopyDirectoryContents (FTPConnection ftpConnection) { this.ftpConnection = ftpConnection; } public InArgument<string> SourceDirectoryName { get; set; } public InArgument<string> TargetDirectoryName { get; set; } protected override void Execute(CodeActivityContext context) { MoveDirectoryContents(SourceDirectoryName.Get(context), TargetDirectoryName.Get(context)); } public void MoveDirectoryContents(string sourceDirectoryName, string targetDirectoryName) { DirectoryInfo directoryInfo = new DirectoryInfo(sourceDirectoryName); ftpConnection.ChangeWorkingDirectory(targetDirectoryName); foreach (FileInfo fileInfo in directoryInfo.GetFiles()) { ftpConnection.UploadFile(fileInfo.FullName, fileInfo.Name); } foreach (DirectoryInfo subDirectoryInfo in directoryInfo.GetDirectories()) { ftpConnection.CreateDirectory(subDirectoryInfo.Name); MoveDirectoryContents(subDirectoryInfo.FullName, subDirectoryInfo.Name); } ftpConnection.ChangeWorkingDirectoryUp(); } }

I then wired them up in a sequence activity and added a variable to hold the FTPConnection that will be created by the 1st task and then passed to the second and third task:

image

I then assigned values to the ConnectToHost parameters:

image

Note that FTPConnection is an InOutArgument so I can assign it from the higher level sequence.

I then added the arguments to the CopySite Task. Note that the designer handles the slashes (I couldn’t use the @”” syntax):

image

I got an error when I realized I don’t need to use the constructors, rather I need to use the In and OutParameters like so:

1) Create the ftpConnection and pass it out:

FTPConnection _ftpConnnection = null; public InArgument<string> HostName { get; set; } public InArgument<string> UserName { get; set; } public InArgument<string> Password { get; set; } public OutArgument<FTPConnection> FTPConnection { get; set; } protected override void Execute(CodeActivityContext context) { _ftpConnnection = new FTPConnection(); _ftpConnnection.ServerAddress = HostName.Get(context); _ftpConnnection.UserName = UserName.Get(context); _ftpConnnection.Password = Password.Get(context); _ftpConnnection.Connect(); FTPConnection.Set(context, _ftpConnnection); } }

2) Store the FTPConnection in the SequenceActivity’s local variable.

3) Get the FTP as an InArgument and use it:

public InArgument<string> SourceDirectoryName { get; set; } public InArgument<string> TargetDirectoryName { get; set; } public InArgument<FTPConnection> FTPConnection { get; set; } protected override void Execute(CodeActivityContext context) { ftpConnection = FTPConnection.Get(context); MoveDirectoryContents(SourceDirectoryName.Get(context), TargetDirectoryName.Get(context)); }

And boom goes the dynamite! It worked and the files moved over. I can now set up the FTP tasks in the TFS Build Activity.

FTP Task in WF and TFS

Now that I have custom MSBuild tasks working using WF (and kicking them off using TFS), I realized that I DON”T want to use custom MSBuild tasks.  It seems easier just to put in a custom WF task as part of the bdefult build template that comes out of the box in TFS.  To that end, I created a new template and identified the step where I would put in this new workflow task (it took 5 minutes to click through this TFS WF, it is a beast!):

image

 

I then created a new project to get the FTP tasks working.  Before using WF, I thought of just creating a Console app that does what I want. I monkeyed around with the the native .NET FTP library but is stinks and then I tried FTPLibrary – which didn’t work out of the box. I then tried EditFTP and it worked great so I used that.  I created a structured program that uses the Edit FTP API to copy the contents from the build directory to the FTP site (oooh, recursion):

static FTPConnection ftpConnection = null; static string hostName = @"ftp.xxx.com"; static string userName = "xxx"; static string password = "xxx"; static string sourceLocation = @"xxx"; static string targetLocation = "xxx"; static void Main() { Console.WriteLine("Start"); ConfigureFTPConnnection(); CopyDirectoryContents(sourceLocation, targetLocation); Console.WriteLine("End"); Console.ReadKey(); } static void ConfigureFTPConnnection() { ftpConnection = new FTPConnection(); ftpConnection.UserName = userName; ftpConnection.Password = password; ftpConnection.ServerAddress = hostName; ftpConnection.Connect(); } static void CopyDirectoryContents(string sourceDirectoryName, string targetDirectoryName) { DirectoryInfo directoryInfo = new DirectoryInfo(sourceDirectoryName); ftpConnection.ChangeWorkingDirectory(targetDirectoryName); foreach (FileInfo fileInfo in directoryInfo.GetFiles()) { ftpConnection.UploadFile(fileInfo.FullName, fileInfo.Name); } foreach (DirectoryInfo subDirectoryInfo in directoryInfo.GetDirectories()) { ftpConnection.CreateDirectory(subDirectoryInfo.Name); CopyDirectoryContents(subDirectoryInfo.FullName, subDirectoryInfo.Name); } ftpConnection.ChangeWorkingDirectoryUp(); }

Pretty simple stuff – note the use of ChangingWorkingDirectoryUp to keep the current directory on the remote site synched.

I then thought about how to create a WF class that does the same thing.  To do that, I fired up a VS2010 Activity diagram.  My 1st cut was WAAY too complicated:

image

I refined it based on the fact that my FTP API automagically overwrites files:

image

Much easier.  I then created a workflow activity (with its .asmx extension).  I realized that I would simply throw a code activity on to the designer, move the procedureal code I already wrote into that activity, and call it a a day.  However, I wanted to see if I can exploit the power of WF and replicate the activity diagram using workflow tasks.  I’ll document my experiences with that attempt next week.

MSBuild Custom Task and TFS

I tried to kick off the a custom build task using TFS like so:

image

The .csproj has 1 file:

image

And the 1 class has the following code:

using System; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace Com.Tff.Carpool.Build { public class PublishWebSite : Task { public override bool Execute() { return true; } } }

However, the build just hangs:

image

I then realized I never included the Build in the .csproj definition. I unloaded the project and added the following line of code:

image

I then reloaded the project and it still just hanged. I then realized that my Build Controller was not running <sound of me slapping my head />

image

Once I started the build controller, the job kicked off and ran:

image

I then added the workflow from this previous post to the Build and it ran.   I then went to the build definition’s Process tab and realized I was using the Default template:

image 

It seems to me that the path of least resistance would be to have a couple of WF tasks in the Template that takes the website and FTP it out. I would do this versus creating a new project that has the workflow and then has to find the files in the drop folder.  Therefore, no MSBUild tasks.

I first created a new template based on the default:

image

Note that I have to manually add the template to source control:

image

I then double clicked on it to alter the definition- and waited 15 seconds for the screen to refresh – holy cow this workflow is a beast!

image

I want to add 1 task at the end – ftp contents to a remote directory. I wonder how I am going to unit test this. I realized that I need to create the FTP Workflow in a different project, test it, and then move it over into this template.  That will be the subject of my next post.

MSBuild and Workflow Foundation

I am in the process of investigating how to create a FTP Task to be used in the TFS Build Engine.  This post is about creating a MSBuild task, kicking it off, and then integrating WF into that task. 

Using this MSDN article, I crated a basic task like so:

using System; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; namespace Tff.FtpTask { public class PublishWebsite: Task { public override bool Execute() { return true; } } }

I then created a build file like so:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Target Name="PublishWebsite"> <PublishWebsite /> </Target> </Project>

When I ran it:

image

The build file is not finding the task I created. I altered the build file like this:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <UsingTask TaskName="PublishWebsite" AssemblyFile="C:\Users\Jamie\Documents\Visual Studio 2010\Projects\Tff.FtpTask\Tff.FtpTask\bin\Debug\Tff.FtpTask.dll" /> <Target Name="PublishWebsite"> <PublishWebsite /> </Target> </Project>

And pop goes the fire cracker:

image

I then added in a workflow to the project:

image

Called the workflow:

public override bool Execute() { WorkflowInvoker.Invoke(new PublishWebsite()); return true; }

And Bang!  That was pretty straight forward.

image