Sunday, October 18, 2009

Working with the 1.5 code base : part 5

Away with the dreaded WPF, and back to good old CCNet coding ;-)
This part will cover another updated part of the code : communication. In 1.5 Craig introduced messaging, Changing to Messages. As always Craigs blog is very informative in what and why he changes. I'll try to give a more hands-on example, so others can see more details and some code.
The problem : In CCNet 1.4.3 I exposed the breakers of a build to cctray and the dashboard. Cradiator also uses this information. Now in CCNet 1.5 Craig, yes he again, exposed also the breaking tasks, which is good. But this introduced a problem : the xml feed Cradiator uses, only exposes the last message! So this means that we show the breaker, OR the breaking tasks. (also the dashboard has this problem for the moment)
The solution is simple : expose all messages :-) This is covered in Jira Issue 1718
Anyway, this kind of information is stored in the messages arraylist on the project being integrated. When a build is ok, we clear this list, when a build is bad, we look at the modifications, and extract the user names. The same goes for the tasks that failed. All this information is just a message in this arraylist. This also is a problem on its own. A message is just a text : John Wayne Broke the build. You can not see what kind of information it is, only by parsing the text :-(

Fixing the issue :
Step 1 : add a type for each message
Currently I have these 4 types in an enum
  • NotDefined
  • Breakers
  • Fixer
  • FailingTasks
This is just adding a property with the enum to the message class

/// <summary>
/// The type of message
/// </summary>
[XmlText]
public MessageKind Kind
{
get { return messageKind; }
set { messageKind = value; }
}
Step 2 : adjusting the communication
We need to update the MessageRequest to have this new property also

/// <summary>
/// The kind of message
/// </summary>
[XmlElement("kind")]
public Message.MessageKind Kind
{
get { return kind; }
set { kind = value; }
}
Everywhere where we go from a MessageRequest to a Message we need to pass this new property value. This is in 1 place : CruiseServer.cs in public virtual Response SendMessage(MessageRequest request)
The only thing left to do is specify the value of the type when we add a message to the list. For CCTray this is the option :Volunteer to fix build, in the integration itself, we must add the correct value to the breakers and the failing tasks.

Step 3 : expose the messagelist

This is merely adding a new xml element, which is a list, nothing special.
So in XmlReportAction.cs add this element :
xmlWriter.WriteStartElement("messages");

foreach (Message m in status.Messages)
{
xmlWriter.WriteStartElement("message");
xmlWriter.WriteAttributeString("text", m.Text);
xmlWriter.WriteAttributeString("kind", m.Kind.ToString());
xmlWriter.WriteEndElement();
}

xmlWriter.WriteEndElement();


Not so hard was it ?

Stay tuned for more in the future ...

Thursday, October 15, 2009

Wpf : Final encounter

I tried to enhance the display so that issues with attachments would have an icon (paperclip) next to it. To show and hide this image is easy, just use a converter. But getting the image to use an embedded resource was more difficult. For me this was the last drop.
I quit this WPF stuff for the moment, maybe I take it up again later, when the tools are more to todays standard, but from what I heard about VS2010, I doubt that it will be in the near future. Anyone who wants to take a look at the code, you can find the project at : Source Forge project
svn repository : https://jirawpf.svn.sourceforge.net/svnroot/jirawpf

maybe it needs some more comments, but you should get it to work.
If there are questions about the project itself, feel free to ask, I'll try to answer them.

Back to CCNet, that is a project I've neglected a bit, and needs some attention.

Friday, October 2, 2009

Wpf : style issues

Got the program almost like I want it. Encountered another big annoying WPF issue :
the order of the styles and datatemplates in a XAML file is of importance !
Take a look at the following example :
<DataTemplate x:Key="IssueDataTemplate" >
<ContentControl Style="{StaticResource IssueTemplate}"/>
</DataTemplate >

<Style x:Key="IssueTemplate" >
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal" >
<Label Content="{Binding Key}" Width="200"/>
<Label Content="{Binding Description}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
You get a freaking runtime error : Unable to find style IssueTemplate ! ! !

To fix it, place the style first in the file, and next the datatemplate.

Questions :
1) Why does it throw a runtime error, and not a compile error, it is a static resource, meaning that it is known at design time, it is not loaded via an assembly at runtime (late binding technology).
2) Why an error in the first place? Back in 1990, it was of importance to place the procedures in correct order in the code, so the compiler could find them. But we're now in 2009! This is the way of working of 20 years ago!

When you code in any .Net language, Delphi, C, VB6, VB5, VB4 or even COBOL the place of a function in a class does not matter, hey even with partial classes, it does not even matter in which file they are! But WPF fails if the order is not ok.

I have a feeling that this issue will make the style files unorganized, and so making it errorprone :-(

sigh me wonder what is so fun and exciting about wpf, can someone enlighten me ? I do not grasp it.

Getting it to work

Found the courage again to code in this WPF world.
Reverted to use the ItemTemplate in the listbox, to get it to work. But now I have double XAML code : a datatemplate showing how a certain item is displayed, and a style showing how a certain item is displayed. The solution for this is to let the datatemplate use the style, so the layout is only defined once, like so :
<Style x:Key="IssueTemplate" >
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal" >
<Label Content="{Binding Key}" Width="200"/>
<Label Content="{Binding Description}" />
<Label Content="{Binding Votes}" />
<Label Content="{Binding Assignee}" />
<Label Content="{Binding Created}" />
<Label Content="{Binding Priority}" />
<Label Content="{Binding Reporter}" />
<Label Content="{Binding Status}" />
<Label Content="{Binding Votes}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<DataTemplate x:Key="IssueDataTemplate" >
<ContentControl Style="{StaticResource IssueTemplate}"/>
</DataTemplate >
The issue with the double click is not relevant anymore, I wanted to show a second window on double clicking an issue, but changed my mind. Now when you select an item in the listbox, the details of the selected issue are shown at the top of the screen. This is done by adding a ContentControl to the main window, binding its datacontext it to the same one as the listbox and setting the property IsSynchronizedWithCurrentItem to true. This works rather well. The code of the main window looks now like this :
<DockPanel>
<Expander VerticalContentAlignment="Top"
DockPanel.Dock="Left"
DataContext="{StaticResource CCNetProject}" >
<ContentControl Style="{StaticResource CCNetProjectTemplate}" />
</Expander>
<ContentControl DataContext="{StaticResource AllIssues}"
Style="{StaticResource IssueDetailTemplate}"
DockPanel.Dock="Top" />
<ListBox DockPanel.Dock="Bottom"
ItemsSource="{Binding Source={StaticResource AllIssues}}"
ItemTemplate="{StaticResource IssueDataTemplate}"
IsSynchronizedWithCurrentItem="True"
/>
</DockPanel>

As you can see, there is nothing in the main window about how a certain item is displayed, no coloring, borders, ... All this is done is in the styles. I'll see how far I get with this approach. I'm now busy with showing the details of a certain issue, when that is done the most important functionality of the program is done, making it possible to give the app a more polished look.
Stay tuned ...

Friday, September 11, 2009

Struggling with basic behaviour

Ran into some problems again :-(.
1) If I populate the listbox with lot's of items, let's say 200, it falls of the screen!
In winforms I would expect a scrollbar to appear, but not in my WPF app :-(. So I added a Scrollviewer around the listbox, but to no avail.
After looking around on the net, it appears that the stackpanel is at fault, my Listbox resides in a StackPanel, and this combination makes the items of the listbox to go beyond the window edge.
Solution : remove the stackpanel, this was not realy needed (yet) for my program, and when I do need a container control in the future, I'll use a grid

2) Standard a listbox does not support double-click
Surfs up : listbox double click. You have to manually add the event.

3) When I use the ItemContainerStyle in stead of the ItemTemplate, like said in the previous post, the listbox looses the ability to select items somehow. Go figure. If there is a soul out there, that can explain me the difference between ItemContainerStyle and ItemTemplate, please enlighten me.

Now these are the kind of things that drive me nuts when I am programming in WPF !
When coming from Windows Forms, I keep on stumbling on these basic stuff that is natively in windows forms, but not in WPF, I think I'll join the WPF Hate team, and stick to console and winforms. At least those work, no eye candy, but I can say it'll be finished in 2 days.

I mean it is easy to create Star Wars like stuff in WPF, but real working apps, that remains to be seen. Ok, there are a lot of showcases to be found on the Net, but I wonder how many devs and designers where put on those projects. Also read this page

laying off for a while, getting too angry ...

Thursday, September 10, 2009

Keeping the overview

The main window is getting into shape, but I do not like the Xaml code :-(. It's getting big, and I'm loosing the overview. Below is a small example of what I mean, the window just shows the CCNet project data, (name, description, ...) and a list of the issues (key, description, votes, ..)
<Window x:Class="JiraWpf.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lc="clr-namespace:JiraWpf"
Title="Window2" >
<Window.Resources>
<ObjectDataProvider x:Key="CCNetProject"
ObjectType="{x:Type lc:CCNetJiraDataProvider}"
MethodName="GetCCNetProject"
/>
<ObjectDataProvider x:Key="AllProjects"
ObjectType="{x:Type lc:CCNetJiraDataProvider}"
MethodName="GetProjects"
/>
<ObjectDataProvider x:Key="AllIssues"
ObjectType="{x:Type lc:CCNetJiraDataProvider}"
MethodName="GetIssuesFromFilter"
/>
<DataTemplate x:Key="IssueTemplate" >
<StackPanel Orientation="Horizontal" >
<Label Content="{Binding Key}" Width="200"/>
<Label Content="{Binding Description}" />
<Label Content="{Binding Votes}" />
<Label Content="{Binding Summary}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<StackPanel>
<DockPanel>
<Expander VerticalContentAlignment="Center" DockPanel.Dock="Left" DataContext="{StaticResource CCNetProject}" >
<StackPanel Orientation="Vertical" >
<Label Content="{Binding Lead}" />
<Label Content="{Binding Key}" />
<Label Content="{Binding Description}" />
<Label Content="{Binding Name}" />
</StackPanel>
</Expander>
<ListBox DockPanel.Dock="Top"
ItemsSource="{Binding Source={StaticResource AllIssues}}"
ItemTemplate="{StaticResource IssueTemplate}"
/>
</DockPanel>
</StackPanel>
</Window>
As you can see, that is getting big, and it shows almost nothing :-( Time to clean house!
First thing I do not like is the difference of showing data between the listbox and the expander. I prefer the one of the listbox, where you say that the items have the specified template, leaving the actual layout out of the overview.
Downside is that Expander does not have an ItemTemplate property, and creating that property and functionality on all kind of controls does not sound as an attractive solution. The good news is that does exist already, WPF has this functionality build in : you just have to use basic controls and styles !
Now that is what I like : basic controls. Never thought that I would ever say : I like something about WPF ;-)
So I created a style that just sets the Control.Template property, this allows to easily create a visualisation of a certain item, the way like the it is done with a DataTemplate. And this style is applied to a ContentControl residing in the expander. This is already better, but now I have a style and a datatemplate to visualise data, and personally I do not like 2 ways of doing the same stuff in 1 program. So I changed the DataTemplate also into a style. This means that I have to use the ItemContainerStyle property in stead of the ItemTemplate in the listbox.
That being done, the way of presentation is more consistent, but the file is still rather big. It would be nice if the resources where moved into a separate file, or set of files, and luckily this is also supported out of the box with the ResourceDictionary.MergedDictionaries. Resulting in the following for the window.xaml
<Window x:Class="JiraWpf.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lc="clr-namespace:JiraWpf"
Title="Window1" >
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/DataProvider.xaml" />
<ResourceDictionary Source="Resources/DataLayout.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<StackPanel>
<DockPanel>
<Expander VerticalContentAlignment="Center" DockPanel.Dock="Left" DataContext="{StaticResource CCNetProject}" >
<ContentControl Style="{StaticResource CCNetProjectTemplate}" />
</Expander>

<ListBox DockPanel.Dock="Top"
ItemsSource="{Binding Source={StaticResource AllIssues}}"
ItemContainerStyle="{StaticResource IssueTemplate}"
/>
</DockPanel>
</StackPanel>
</Window>

As you can see, a lot shorter and more easily to follow. Below is the DataLayout.xaml file, I did not post the DataProvider.xaml file, because it is just a copy and paste of all the ObjectDataProviders into an empty ResourceDictionary file.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="CCNetProjectTemplate" >
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Vertical" >
<Label Content="{Binding Lead}" />
<Label Content="{Binding Key}" />
<Label Content="{Binding Description}" />
<Label Content="{Binding Name}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="IssueTemplate" >
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal" >
<Label Content="{Binding Key}" Width="200"/>
<Label Content="{Binding Description}" />
<Label Content="{Binding Votes}" />
<Label Content="{Binding Summary}" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>


Now this looks like it is more manageable. Stay tuned ...

Monday, September 7, 2009

Binding data with INotifyPropertyChanged

Wpf can only bind to properties, and in order to let it automatically refresh the UI when a bound data item changes value, it must implement the INotifyPropertyChanged property.
This means that the following code does not suffice :
using System;
namespace JiraWpf.DataObjects
{
public class Person
{
public Person()
{
}
public string Name { get; set; }
}
}

So let's implement the interface, this leads us to the following :
using System;
using System.ComponentModel;

namespace JiraWpf.DataObjects
{
public class Person : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

public Person()
{ }

private string name;
public string Name
{
get { return name; }
set
{
if (value != this.name)
{
this.name = value;
OnPropertyChanged(new PropertyChangedEventArgs("Name"));
}
}
}

private void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, args);
}
}
}

Not a big change, but if you have a lot of properties, this is very bad. We came from a 1 line property implementation to a 13 line !! The lines for the event and raising it are common for all properties, so they are not counted. Every property compares its value with the previous value, and when those 2 are different it raise the change-event. This is functionality that is the same for all properties so it belongs somewhere else. It would be handy if an attribute NotifyOnChange could be set on a property, but this does not exist (as far as I know). Meaning this comparison must be done in a self made base class : NotifyingObject, leading us to the following :
using System;
using System.ComponentModel;
using System.Collections.Generic;

namespace JiraWpf.DataObjects
{
public class NotifyingObject : INotifyPropertyChanged
{
private Dictionary<string, IComparable> props;

public event PropertyChangedEventHandler PropertyChanged;

public NotifyingObject()
{
props = new Dictionary<string, IComparable>();
}

public void SetValue<T>(string name, T value) where T : IComparable
{
if (!props.ContainsKey(name))
{
props.Add(name, value);
}
else
{
if (props[name].CompareTo(value) != 0)
{
props[name] = value;
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
}

public T GetValue<T>(string name) where T : IComparable
{
return (T)props[name];
}

protected void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, args);
}
}
}
using System;
using System.ComponentModel;

namespace JiraWpf.DataObjects
{
public class Person : NotifyingObject
{
public Person()
{ }

public string Name
{
get { return base.GetValue<string>("Name"); }
set { base.SetValue("Name", value); }
}
}
}

Ok, this is already much better, property definitions are back to just 5 lines, but at least it is readable again. The NotifyingObject has a list of the property names and their values, so on a get, we look up the value in the list. On a set we compare with the value in the list, and when the 2 are different, we raise the event.
Now what still bothers me on this is that we type the property name 3 times in the Person class! Once in the property name itself, secondly in the get and another time in the set. This is asking for trouble on refactoring (renaming the property). No tool will change those names in the get and set, they are hardcoded string values ! This leads us to the following implementation of the 2 classes :
using System;
using System.ComponentModel;
using System.Collections.Generic;

namespace JiraWpf.DataObjects
{
public class NotifyingObject : INotifyPropertyChanged
{
private Dictionary<string, IComparable> props;

public event PropertyChangedEventHandler PropertyChanged;

public NotifyingObject()
{
props = new Dictionary<string, IComparable>();
}

public void SetValue<T>(T value) where T : IComparable
{
string name = GetCallerPropertyName();

if (!props.ContainsKey(name))
{
props.Add(name, value);
}
else
{
if (props[name].CompareTo(value) != 0)
{
props[name] = value;
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
}

public T GetValue<T>() where T : IComparable
{
string name = GetCallerPropertyName();
return (T)props[name];
}

protected void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, args);
}

private string GetCallerPropertyName()
{
System.Diagnostics.StackTrace stack = new System.Diagnostics.StackTrace();
System.Diagnostics.StackFrame currentFrame = stack.GetFrame(2);

//strip of the set_ and get_ prefixes of the generated property names, to get the ones you typed
string propname = currentFrame.GetMethod().Name.Substring(4);

return propname;
}
}
}
using System;
using System.ComponentModel;

namespace JiraWpf.DataObjects
{
public class Person : NotifyingObject
{
public Person()
{ }

public string Name
{
get { return base.GetValue<string>(); }
set { base.SetValue(value); }
}
}
}

We let the NotifyingObject look up the property name itself, preventing us from ever typing in a wrong name. This is now safe for refactoring.
Another benefit of the NotifyingObject is that you could foresee a way to postpone the raise event till all properties are set, a BeginUpdate and EndUpdate function could foresee that. Implementing extra logging is now also much easier. Anyway, with this baseclass ready, I can start working on the next part.
Stay tuned ....

Sunday, September 6, 2009

Wpf : what you really need to understand first is databinding

Learning the basics layout of a WPF program is not that difficult, I mean how to place basic controls like buttons, labels, textboxes, ... Making a great interface is something else, but that requires other skills, UI designer skills. But what I think is the most important part to understand early is the binding. This is the part of the course / book that I advise you to spent the most time on. I've lost too many hours finding out why my data was not visualised :
° Compilation : 0 warnings, 0 errors
° Setting a breakpoint on the method that delivered the data : breakpoint was never hit
° Output window did not show any errors, warnings
° Settings the ItemsSource in code behind works like a charm, so the data providing method works !

Below is an example program, see if you can find the mistake.
<Window x:Class="Damn.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lc="clr-namespace:Damn"
Title="Window1" >

<Window.Resources>
<ObjectDataProvider x:Key="CCNetProject"
ObjectType="{x:Type lc:CCNetJiraDataProvider}"
MethodName="GetProjects"
/>

<DataTemplate x:Key="CCNetProjectTemplate" >
<StackPanel Orientation="Horizontal" Background="LightBlue">
<Label Content="{Binding Key}" Width="200"/>
<Label Content="{Binding Description}" />
</StackPanel>
</DataTemplate>
</Window.Resources>

<StackPanel DockPanel.Dock="Top" Orientation="Vertical">
<ListBox x:Name="lstY"
ItemsSource="{Binding StaticResource CCNetProject}"
ItemTemplate="{StaticResource CCNetProjectTemplate}"/>
</StackPanel>

</Window>

So this has kept me busy for some hours, ok the mistake was mine, but how could I have found the error more easily?
I can not set a breakpoint in XAML, and setting a breakpoint in the method did not get hit. How do other WPF programmers pinpoint these kind of problems. For me this is a serious problem / shortcoming in Visual Studio. Any body knows some tips/tricks ?

Friday, September 4, 2009

Wpf : learning new grounds

Part of my main job involves WPF, which I find hard to learn, coming from winforms and making mostly console utility apps (no events flying around). So I needed a pet project to learn this. I looked at CCNet, it has no WPF frontend, but that already exists, Cradiator aka BVC2, so another candidate had to be found.

A painpoint for me as developper on CCNet is to find items which have patches, our Jira Issue site has many search options, but that one is not provided. The good point is that there is a wsdl connection available to the issue tracker, which has this possibility ! An issue has an Attachements property, which is a list of strings, the filenames!
Basically all I need to do is get all open items, and filter out the ones where Attachements.Length != 0 .

The console version was finished in half an hour, the WPF, I'm still working on :-(
Details will follow in next posts. I'm also taking this project as a way to improve my knowledge of other items like patterns, refactoring, ...
And I want to do this project the 'WPF way', not winform like.

Do not worry, I'll keep working on CCNet to :-)

Monday, August 10, 2009

Continous Installation : How it works

It's been a while since my last post ;-) but here is a new one. This post will give more detailed information of the setup I have to install software on 75 servers, and keep an eye on them. As you know, I use CCNet for distributing our software to our customers (about 70 for the moment). These installations involve click-once applications, database upgrades, server checks, ....

I made a presentation in openoffice and ms office so you can have a look, download the format of your choice and start the presentation (F5). Here's a short overview of the presentation :
  • Company is connected to an external Ftp Server via the Internet
  • Customers have an application server with CCNet pre-installed, which will connect at pre-defined times (schedule trigger) with the Ftp server
  • When there is a need for a change (new installation/ program),we upload a modified ccnet.config and the needed files.
  • When the schedule trigger fires, CCNet.Config is downloaded, and copied to the path where CCNet watches it. Meaning the new configuration will be read
  • If it needs, other files are also downloaded and executed / installed

Now, the 10 point question :
how do we know if an installation was ok, the server was updated, ... ?
At preset times, about 7 times a day, each appserver uploads the state of its CCNet service, this is the data you can see in the dashboard. This data is downloaded to the company and processed.
So we see if the update was ok ('build' ok or failed), and if the update was done or not, via the last executed time.

Friday, June 26, 2009

Working with the 1.5 code base : part 4

In this part I will cover the FTP Source Control. At the base it is not that hard, since most of the stuff we already have :-). I added a class 'FtpSourceControl' that implements ISourceControl. This interface has the following definition:
public interface ISourceControl
{
Modification[] GetModifications(IIntegrationResult from, IIntegrationResult to);

void LabelSourceControl(IIntegrationResult result);
void GetSource(IIntegrationResult result);

void Initialize(IProject project);
void Purge(IProject project);
}

The only item we do not have is the GetModifications in IFtpLib, so this needs an update : Modification[] ListNewOrUpdatedFilesAtFtpSite(string localFolder, string remoteFolder, bool recursive);
The implementation of this method, is also very easy, it is the same as the download, except that it does not need to retieve the file, only list it.
And that is it, easy. Ok, this code still needs some cleanup, but it shows how you can create new functionality for CCNet, step by step.
The part where you are waiting on :

The code

IFtpLib
using System;
namespace ThoughtWorks.CruiseControl.Core.Util
{
interface IFtpLib
{
/// <summary>
/// Logs into the specified server, with the userName and password
/// If activeConnectionMode is set to true, active connection is used,
/// otherwise passive connection.
/// </summary>
/// <param name="serverName"></param>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <param name="activeConnectionMode"></param>
void LogIn(string serverName, string userName, string password, bool activeConnectionMode);

/// <summary>
/// Disconnects from the server
/// </summary>
void DisConnect();

/// <summary>
/// returns true if connected
/// </summary>
/// <returns></returns>
bool IsConnected();

/// <summary>
/// returns the current path of the server
/// </summary>
/// <returns></returns>
string CurrentWorkingFolder();

/// <summary>
/// downloads the remoter folder to the local folder, recursive if wanted
/// </summary>
/// <param name="localFolder"></param>
/// <param name="remoteFolder"></param>
/// <param name="recursive"></param>
void DownloadFolder(string localFolder, string remoteFolder, bool recursive);

/// <summary>
/// Uploads a local folder to the specified remotefolder, recursive if wanted
/// </summary>
/// <param name="remoteFolder"></param>
/// <param name="localFolder"></param>
/// <param name="recursive"></param>
void UploadFolder(string remoteFolder, string localFolder, bool recursive);

/// <summary>
/// Returns a list of new or updated files at the ftp site, compared to a local folder
/// </summary>
/// <param name="localFolder"></param>
/// <param name="remoteFolder"></param>
/// <param name="recursive"></param>
/// <returns></returns>
Modification[] ListNewOrUpdatedFilesAtFtpSite(string localFolder, string remoteFolder, bool recursive);
}
}
FtpLib
namespace ThoughtWorks.CruiseControl.Core.Util
{
public class FtpLib : IFtpLib
{
private EnterpriseDT.Net.Ftp.FTPConnection FtpServer;
private Tasks.TaskBase CallingTask;
private Util.BuildProgressInformation bpi;

public FtpLib(Tasks.TaskBase callingTask, Util.BuildProgressInformation buildProgressInformation)
{
CallingTask = callingTask;
bpi = buildProgressInformation;

this.FtpServer = new EnterpriseDT.Net.Ftp.FTPConnection();

this.FtpServer.ReplyReceived += HandleMessages;

this.FtpServer.CommandSent += HandleMessages;

this.FtpServer.Downloaded += new EnterpriseDT.Net.Ftp.FTPFileTransferEventHandler(FtpServer_Downloaded);

this.FtpServer.Uploaded += new EnterpriseDT.Net.Ftp.FTPFileTransferEventHandler(FtpServer_Uploaded);

}

public FtpLib(Util.BuildProgressInformation buildProgressInformation)
{
bpi = buildProgressInformation;

this.FtpServer = new EnterpriseDT.Net.Ftp.FTPConnection();

this.FtpServer.ReplyReceived += HandleMessages;

this.FtpServer.CommandSent += HandleMessages;
}

public FtpLib()
{
this.FtpServer = new EnterpriseDT.Net.Ftp.FTPConnection();

this.FtpServer.ReplyReceived += HandleMessages;

this.FtpServer.CommandSent += HandleMessages;

}

public void LogIn(string serverName, string userName, string password, bool activeConnectionMode)
{

Log.Info("Connecting to {0} ...", serverName);

{
this.FtpServer.ServerAddress = serverName;
this.FtpServer.UserName = userName;
this.FtpServer.Password = password;
this.FtpServer.Connect();

if (activeConnectionMode)
{
Log.Debug("Active mode enabled");
this.FtpServer.ConnectMode = EnterpriseDT.Net.Ftp.FTPConnectMode.ACTIVE;
}
else
{
Log.Debug("Passive mode enabled");
this.FtpServer.ConnectMode = EnterpriseDT.Net.Ftp.FTPConnectMode.PASV;
}

this.FtpServer.TransferType = EnterpriseDT.Net.Ftp.FTPTransferType.BINARY;
}
}

public void DownloadFolder(string localFolder, string remoteFolder, bool recursive)
{

this.FtpServer.ChangeWorkingDirectory(remoteFolder);

EnterpriseDT.Net.Ftp.FTPFile[] FtpServerFileInfo = this.FtpServer.GetFileInfos();

string LocalTargetFolder = null;
string FtpTargetFolder = null;
bool DownloadFile = false;
string LocalFile = null;
System.IO.FileInfo fi = default(System.IO.FileInfo);

if (!System.IO.Directory.Exists(localFolder))
{
Log.Debug("creating {0}", localFolder);
System.IO.Directory.CreateDirectory(localFolder);
}

foreach (EnterpriseDT.Net.Ftp.FTPFile CurrentFileOrDirectory in FtpServerFileInfo)
{
if (recursive)
{
if (CurrentFileOrDirectory.Dir && CurrentFileOrDirectory.Name != "." && CurrentFileOrDirectory.Name != "..")
{

LocalTargetFolder = System.IO.Path.Combine(localFolder, CurrentFileOrDirectory.Name);
FtpTargetFolder = string.Format("{0}/{1}", remoteFolder, CurrentFileOrDirectory.Name);

if (!System.IO.Directory.Exists(LocalTargetFolder))
{
Log.Debug("creating {0}", LocalTargetFolder);
System.IO.Directory.CreateDirectory(LocalTargetFolder);
}

DownloadFolder(LocalTargetFolder, FtpTargetFolder, recursive);

//set the ftp working folder back to the correct value
this.FtpServer.ChangeWorkingDirectory(remoteFolder);
}
}

if (!CurrentFileOrDirectory.Dir)
{
DownloadFile = false;

LocalFile = System.IO.Path.Combine(localFolder, CurrentFileOrDirectory.Name);


// check file existence
if (!System.IO.File.Exists(LocalFile))
{
DownloadFile = true;
}
else
{
//check file size
fi = new System.IO.FileInfo(LocalFile);
if (CurrentFileOrDirectory.Size != fi.Length)
{
DownloadFile = true;
System.IO.File.Delete(LocalFile);
}
else
{
//check modification time
if (CurrentFileOrDirectory.LastModified != fi.CreationTime)
{
DownloadFile = true;
System.IO.File.Delete(LocalFile);

}
}
}

if (DownloadFile)
{
Log.Debug("Downloading {0}", CurrentFileOrDirectory.Name);
this.FtpServer.DownloadFile(localFolder, CurrentFileOrDirectory.Name);

fi = new System.IO.FileInfo(LocalFile);
fi.CreationTime = CurrentFileOrDirectory.LastModified;
fi.LastAccessTime = CurrentFileOrDirectory.LastModified;
fi.LastWriteTime = CurrentFileOrDirectory.LastModified;
}

}

}
}

public void UploadFolder(string remoteFolder, string localFolder, bool recursive)
{
string[] LocalFiles = null;

LocalFiles = System.IO.Directory.GetFiles(localFolder, "*.*");
this.FtpServer.ChangeWorkingDirectory(remoteFolder);

// remove the local folder value, so we can work relative
for (int i = 0; i <= LocalFiles.Length - 1; i++)
{
LocalFiles[i] = LocalFiles[i].Remove(0, localFolder.Length + 1);
}

//upload files
//FtpServer.Exists throws an error, so we must do it ourselves
EnterpriseDT.Net.Ftp.FTPFile[] FtpServerFileInfo = this.FtpServer.GetFileInfos();

foreach (var LocalFile in LocalFiles)
{
if (!FileExistsAtFtp(FtpServerFileInfo, LocalFile))
{
this.FtpServer.UploadFile(System.IO.Path.Combine(localFolder, LocalFile), LocalFile);
}
else
{
if (FileIsDifferentAtFtp(FtpServerFileInfo, LocalFile, localFolder))
{
this.FtpServer.DeleteFile(LocalFile);
this.FtpServer.UploadFile(System.IO.Path.Combine(localFolder, LocalFile), LocalFile);
}
}
}

if (!recursive) return;

//upload folders
string[] Folders = null;

string LocalTargetFolder = null;
string FtpTargetFolder = null;

Folders = System.IO.Directory.GetDirectories(localFolder);

// remove the local folder value, so we can work relative
for (int i = 0; i <= Folders.Length - 1; i++)
{
Folders[i] = Folders[i].Remove(0, localFolder.Length + 1);
}

foreach (var Folder in Folders)
{
//explicit set the folder back, because of recursive calls
this.FtpServer.ChangeWorkingDirectory(remoteFolder);

if (!FolderExistsAtFtp(FtpServerFileInfo, Folder))
{
this.FtpServer.CreateDirectory(Folder);
}

LocalTargetFolder = System.IO.Path.Combine(localFolder, Folder);
FtpTargetFolder = string.Format("{0}/{1}", remoteFolder, Folder);

UploadFolder(FtpTargetFolder, LocalTargetFolder, recursive);
}
}

public void DisConnect()
{
this.FtpServer.Close();
}

public bool IsConnected()
{
return this.FtpServer.IsConnected;
}

public string CurrentWorkingFolder()
{
return this.FtpServer.ServerDirectory;
}

public Modification[] ListNewOrUpdatedFilesAtFtpSite(string localFolder, string remoteFolder, bool recursive)
{
System.Collections.Generic.List<Modification> mods = new System.Collections.Generic.List<Modification>();

GetTheList(mods, localFolder, remoteFolder, recursive);

return mods.ToArray();
}

private void GetTheList(System.Collections.Generic.List<Modification> mods, string localFolder, string remoteFolder, bool recursive)
{
this.FtpServer.ChangeWorkingDirectory(remoteFolder);

EnterpriseDT.Net.Ftp.FTPFile[] FtpServerFileInfo = this.FtpServer.GetFileInfos();

string LocalTargetFolder = null;
string FtpTargetFolder = null;
bool DownloadFile = false;
string LocalFile = null;
System.IO.FileInfo fi = default(System.IO.FileInfo);

if (!System.IO.Directory.Exists(localFolder))
{
Log.Debug("creating {0}", localFolder);
System.IO.Directory.CreateDirectory(localFolder);
}

foreach (EnterpriseDT.Net.Ftp.FTPFile CurrentFileOrDirectory in FtpServerFileInfo)
{
if (recursive)
{
if (CurrentFileOrDirectory.Dir && CurrentFileOrDirectory.Name != "." && CurrentFileOrDirectory.Name != "..")
{

LocalTargetFolder = System.IO.Path.Combine(localFolder, CurrentFileOrDirectory.Name);
FtpTargetFolder = string.Format("{0}/{1}", remoteFolder, CurrentFileOrDirectory.Name);

if (!System.IO.Directory.Exists(LocalTargetFolder))
{
Log.Debug("creating {0}", LocalTargetFolder);
System.IO.Directory.CreateDirectory(LocalTargetFolder);
}

GetTheList(mods, LocalTargetFolder, FtpTargetFolder, recursive);

//set the ftp working folder back to the correct value
this.FtpServer.ChangeWorkingDirectory(remoteFolder);
}
}

if (!CurrentFileOrDirectory.Dir)
{
DownloadFile = false;
Modification m = new Modification();

LocalFile = System.IO.Path.Combine(localFolder, CurrentFileOrDirectory.Name);


// check file existence
if (!System.IO.File.Exists(LocalFile))
{
DownloadFile = true;
m.Type = "added";
}
else
{
//check file size
fi = new System.IO.FileInfo(LocalFile);
if (CurrentFileOrDirectory.Size != fi.Length)
{
DownloadFile = true;
m.Type = "Updated";
}
else
{
//check modification time
if (CurrentFileOrDirectory.LastModified != fi.CreationTime)
{
DownloadFile = true;
m.Type = "Updated";
}
}
}

if (DownloadFile)
{
m.FileName = CurrentFileOrDirectory.Name;
m.FolderName = remoteFolder;
m.ModifiedTime = CurrentFileOrDirectory.LastModified;

mods.Add(m);
}
}

}

}

private bool FileExistsAtFtp(EnterpriseDT.Net.Ftp.FTPFile[] ftpServerFileInfo, string localFileName)
{

bool Found = false;

foreach (EnterpriseDT.Net.Ftp.FTPFile CurrentFileOrDirectory in ftpServerFileInfo)
{
if (!CurrentFileOrDirectory.Dir && CurrentFileOrDirectory.Name.ToLower() == localFileName.ToLower())
{
Found = true;
}
}

return Found;
}

private bool FolderExistsAtFtp(EnterpriseDT.Net.Ftp.FTPFile[] ftpServerFileInfo, string localFileName)
{

bool Found = false;
string updatedFolderName = null;

foreach (EnterpriseDT.Net.Ftp.FTPFile CurrentFileOrDirectory in ftpServerFileInfo)
{
if (CurrentFileOrDirectory.Name.EndsWith("/"))
{
updatedFolderName = CurrentFileOrDirectory.Name.Remove(CurrentFileOrDirectory.Name.Length - 1, 1);
}
else
{
updatedFolderName = CurrentFileOrDirectory.Name;
}

if (CurrentFileOrDirectory.Dir && updatedFolderName.ToLower() == localFileName.ToLower())
{
Found = true;
}
}

return Found;
}

private bool FileIsDifferentAtFtp(EnterpriseDT.Net.Ftp.FTPFile[] ftpServerFileInfo, string localFile, string localFolder)
{
bool isDifferent = false;
System.IO.FileInfo fi = default(System.IO.FileInfo);

foreach (EnterpriseDT.Net.Ftp.FTPFile CurrentFileOrDirectory in ftpServerFileInfo)
{
if (!CurrentFileOrDirectory.Dir && CurrentFileOrDirectory.Name.ToLower() == localFile.ToLower())
{
fi = new System.IO.FileInfo(System.IO.Path.Combine(localFolder, localFile));

if (fi.Length != CurrentFileOrDirectory.Size || fi.LastWriteTime != CurrentFileOrDirectory.LastModified)
{
isDifferent = true;
}
}
}

return isDifferent;
}

private void HandleMessages(object sender, EnterpriseDT.Net.Ftp.FTPMessageEventArgs e)
{
bpi.AddTaskInformation(e.Message);

Log.Debug(e.Message);
}

private void FtpServer_Uploaded(object sender, EnterpriseDT.Net.Ftp.FTPFileTransferEventArgs e)
{
string file;
if (!e.RemoteDirectory.EndsWith("/"))
file = string.Concat("Uploaded : ", e.RemoteDirectory, "/", e.RemoteFile);
else
file = string.Concat("Uploaded : ", e.RemoteDirectory, e.RemoteFile);

AddTaskStatusItem(file);
}

private void FtpServer_Downloaded(object sender, EnterpriseDT.Net.Ftp.FTPFileTransferEventArgs e)
{
string file;
if (!e.RemoteDirectory.EndsWith("/"))
file = string.Concat("Downloaded : ", e.RemoteDirectory, "/", e.RemoteFile);
else
file = string.Concat("Downloaded : ", e.RemoteDirectory, e.RemoteFile);

AddTaskStatusItem(file);
}

private void AddTaskStatusItem(string information)
{
CallingTask.CurrentStatus.AddChild(new ThoughtWorks.CruiseControl.Remote.ItemStatus(information));

if (CallingTask.CurrentStatus.ChildItems.Count > 10)
{
CallingTask.CurrentStatus.ChildItems.RemoveAt(0);
}

}
}
}
FtpSourceControl


using System;
using System.Collections.Generic;
using System.IO;
using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core.Util;

namespace ThoughtWorks.CruiseControl.Core.Sourcecontrol
{
[ReflectorType("ftpSourceControl")]
public class FtpSourceControl : ISourceControl
{
private FtpLib ftp;

[ReflectorProperty("serverName", Required = true)]
public string ServerName = string.Empty;

[ReflectorProperty("userName", Required = true)]
public string UserName = string.Empty;

[ReflectorProperty("password", Required = true)]
public string Password = string.Empty;

[ReflectorProperty("useActiveConnectionMode", Required = false)]
public bool UseActiveConnectionMode = true;

[ReflectorProperty("ftpFolderName", Required = true)]
public string FtpFolderName = string.Empty;

[ReflectorProperty("localFolderName", Required = true)]
public string LocalFolderName = string.Empty;

[ReflectorProperty("recursiveCopy", Required = true)]
public bool RecursiveCopy = true;

#region ISourceControl Members

public Modification[] GetModifications(IIntegrationResult from, IIntegrationResult to)
{
ftp = new FtpLib(to.BuildProgressInformation);
string remoteFolder = FtpFolderName;

ftp.LogIn(ServerName,UserName,Password,UseActiveConnectionMode);

if (!FtpFolderName.StartsWith("/"))
{
remoteFolder = System.IO.Path.Combine(ftp.CurrentWorkingFolder(), FtpFolderName);
}

Modification[] mods = ftp.ListNewOrUpdatedFilesAtFtpSite(LocalFolderName, remoteFolder, RecursiveCopy);

ftp.DisConnect();

return mods;
}

public void LabelSourceControl(IIntegrationResult result)
{
}

public void GetSource(IIntegrationResult result)
{
Util.Log.Info(result.HasModifications().ToString());

ftp = new FtpLib(result.BuildProgressInformation);
string remoteFolder = FtpFolderName;

ftp.LogIn(ServerName, UserName, Password, UseActiveConnectionMode);


if (!FtpFolderName.StartsWith("/"))
{
remoteFolder = System.IO.Path.Combine(ftp.CurrentWorkingFolder(), FtpFolderName);
}

ftp.DownloadFolder( LocalFolderName, remoteFolder, RecursiveCopy);

ftp.DisConnect();
}

public void Initialize(IProject project)
{
}

public void Purge(IProject project)
{
}

#endregion
}
}