Monday, December 11, 2017

Spectrum DNS Servers

I recently discovered that Spectrum is totally incompetent when it comes to DNS servers.

My ISP is Spectrum, and recent speed tests showed I'm getting over 100mps, which is great. But I kept getting random and intermittent drops when connecting over the internet. At first I thought it was the website I was trying to reach, but my mom, also was experiencing similar problems with unrelated sites, and more severely.

I stumbled upon the idea of checking my DNS configuration--and sure enough both mine and my mom's were using servers managed by Spectrum. I changed them to using Google servers (8.8.8.8 for primary, 8.8.4.4 for secondary) and just like that our problems went away.

Which leaves me to wonder why Spectrum even bothers with a DNS server. The nature of this problem can be difficult to diagnose for a non-technical person, and Spectrum's support center is unlikely to consider checking these servers as the source of the problem - - leaving their customers nothing more than pissed at them.

But anyway, if Spectrum is your ISP, I'd recommend you change the DNS servers you use and avoid the headaches of Spectrum's incompetence. A simple Google search will help you if you don't know how to do this.

Wednesday, October 22, 2014

Is it English?

After recently changing jobs, I was given a major project to redo their website, written in Php, picking up where a contractor had left off.  I had warned them that my Php knowledge was all but nonexistent, but I was tasked to it anyhow, successfully deploying in about 100 days.

The experience taught me something--so long as the programming language is founded in English, there is no problem in learning it.  Php has similarities to C, so was pretty easy.

Tuesday, January 14, 2014

Windows 7 Streaming Media--resolution

Okay--I think I jumped the gun a bit.

Researching things, it looked like I should be able to run Windows Media Center and stream the TV data pretty easily.

But I couldn't get it to work, because I couldn't get that stupid "Turn on Media Streaming" button to work.  I tried everything recommended in all the Google search, and nothing worked.

But I finally found the solution, and it was about as stupid as it gets.  Why no one thought to recommend trying this first, I'll never know.

Solution: Run Windows Media Player as an Administrator.  Then select the "Streaming" and "Turn on streaming" selections.  Make sure you had the Network and Sharing Center closed before you started.  The button should work now.

All the other recommendations sounded crazy and nuts--like WMC and WMP simply got hosed up somehow.  But starting as an administrator makes sense--but it would have been nice if Microsoft was smart enough to have realized this necessity, or that their code was bugged and not properly identifying the need to run as administrator.  Yes--this is a bug that seems relatively common and yes--this is Microsoft's screw-up.

Okay--unless something new pops up--then my media streaming project is complete--I didn't really want to do this anyhow as it felt like I was reinventing the wheel.

TV Tuner Card to Media Streaming project, part 1

Okay, I don't use this blog much, and I probably should.  I originally intended this blog as a place to put great tidbits I learn--to make it easier for me to find in the future. If someone else finds this useful, then great--but otherwise, I really don't care.

So, in the interest of keeping track of things I find useful, I've got a new personal project I'm starting.  Maybe it'll come out to something useful, maybe not, and maybe it'll take forever for me to complete.  But here it is.  I've got other projects I've done for personal benefit that I haven't put any notice in my blog--I might start documenting my efforts in those in other blog posts.

Problem

I have a number of computers.  I have one high-end PC I built myself and one low-end laptop, both running Windows 7 64-bit.  My friend was kind enough to sell me his old PC and laptop for pretty cheap, both running Windows Vista.  I don't know much yet about their processing power, but I can already tell they are not any better than my low-end laptop.  But that's okay, and I am considering using the old PC for Linux.

On the high-end PC I have installed a TV Tuner PCI card.  Brand is irrelevant as there is no API for it.  I want to stream live TV from this tuner to any other PC or laptop on my network, and control the channels from the client PC.

Windows Media Center offered promise of being able to do this, but it does not look hopeful--and possibly overly complicated to get to work (although if someone knows a simple solution, I'm all ears).  I've tried enabling network media streaming, but the stupid button Microsoft offers doesn't do anything, and I've tried everything I've found in Google searches (deleting WMP libraries, uninstalling/reinstalling WMP and WMC, messing with the Windows Firewall)--nothing works.  A simple error message would speak volumes, but apparently MS didn't have the time to provide error messages here--better to simply draw users to a button that does nothing.  I found plenty of others experiencing the same problem--but no solution that works for me.

Anyway, that is the problem--stream live TV from the PCI tuner to another PC and control the tuner from the remote PC.

Solution

Doing this all through Windows Media Center would be great.  I found WMC to be a really nice application.  Easy to schedule recording, pleasing user interface, easy to pause (this was nice on live TV--neat new function for me to experience).  All these features are desirable to have on a remote PC.

What gets me is that the technology is all possible--it isn't even that complex compared to other features that are already available.

In fact, had I been smart, I could have purchased a network-based tuner instead of a PCI tuner and not had anything else to do.  But alas--I wasn't aware of the network-based tuners until it was too late, and I'm cheap--I don't spend any more than I have to.  Heck, I don't even have any premium TV service--it's a waste of money.  My TV is all over-the-air broadcast.

But nothing seems premade to do what I want.

And why even do this when there's Netflix and other internet solutions?  Well for one thing--I'm cheap.  I don't use any subscription services.  I'm so cheap that my internet access is limited--I have a good high-speed access, but cannot use more than 10GB a month--and I regularly run out on the last few days of every month.  Video over internet is simply not an option to me.

Maybe I am alone in this need, which would explain why I have not been able to find a pre-made solution.

So, to solve this problem, the best way is to break it up into parts.

Part 1 is the server, the PC with the tuner.  The Tuner video stream needs converted to a network stream.  Simple TCP/IP is easy.  But the true network solution will depend on the client, so the protocol used might not be so simple.

Part 2 is the client on the remote PC.  Ideally, Windows Media Center would be used, but that would mean being able to use a protocol that WMC understood--and at this stage I have no idea what protocols WMC or any other video streaming client uses or is capable of using.  Alternatives to WMC include VLC, or even building one from the ground-up.  I really don't like the idea of this last one, since WMC has such a good user-interface.  I hate building user-interfaces.  They're all grunt-work.  Don't take any brains--just artistic talent (of which I have none, nor any patience for).  I like building the engines.

For part 1, I found this DirectShow.Net library, which has samples for getting to the video stream of a tuner card and for changing channels.  It also has samples of video players that I should be able to build from, it WMC and VLC don't work out.  It doesn't matter if I can't control the card from WMC or VLC if those options work--I can always build a simple client that connects to the DirectShow.net library application I build on the server PC to control the tuner (I'd just invent a simple proprietary protocol for handling that--maybe use WCF, or just use standard System.Net stuff).

All the features that I need include being able to change channels and pausing the live stream.  Recording is moot--I can just use WMC directly on the server PC for that--the remote capability for this is irrelevant (except in the case of an ad hoc decision).  If I can use WMC or VLC on the client, they automatically have the capability of pausing, but they might not have the capability to change channels (which is where a special control client will come in).  If I have to spin my own client, adding pause might not be straight-forward.  To make this possible, then going from network stream to video stream, I really need to cache the network stream first, and a question is, can I cache to disk fast enough for the video stream to not have a problem.  The cache will need opened for read and write.  This will be a bit of new territory--I've only worked with full file streams over FTP protocol--I've never started a read on a stream while it is still being written to.  An additional complexity is to consider when the cache elements should be deleted.  In a worst case scenario, if I am watching one stream for hours--do I keep that stream untouched on disk the entire time (giving me the ability to restart from the beginning), or do I delete viewed parts of the stream after a period of time?  Since it would be easier to not deal with it, and since disk space is not really of concern, I would probably only purge the cache when the channel is changed or the client is closed, or the stream is stopped.  If recording is desired to be added to this custom client, scheduling should not be implemented, since it can be handled better in 
WMC.  Recording can be simply a matter of marking start and stop points in the cache and copying out the parts of the cache between those points.

Okay--a bit of rambling as my brain spills out.  My next task will be to drop the DirectShow library into a C# project and examine the samples.  After that, I need to figure out what the protocol is that WMC can use.  I already know that VLC can use a few different protocols, including HTTP, but its interface wasn't as pleasing, and it seemed a bit more difficult to configure for the stream.  Of course, I am the kind of person the firmly believes that if a manual is needed for basic functionality, then the product was not designed correctly.  You shouldn't have to use a manual to know how to plug a TV into the power outlet and where to put the video cable in and start watching your TV--a manual is okay for more advanced functions, but if the TV were so complex that you had to pull out the manual just to figure out how to give it A/C power, then the designer should face an execution squad.

But anyway, I'll post updates as I make progress on this project.  The updates may come quickly or they may be few and far between--it all depends on my success and how much time I devote to this.

Thursday, October 17, 2013

Hoaxes

I've got a couple of blogs, and I don't really post much on this technical blog.  I use this one mainly for myself--to make it easier for me to find my own little tricks.

But I thought I'd get on a soapbox and speak out against something that has been bothering me lately--Facebook Hoaxes.

Now, I value my privacy, but I also like to remain connected to my friends.  I'm on Facebook as an easy way to keep in touch, but I also don't post much to it.  I don't go posting about what I had for lunch--and get annoyed when others think I care about the minutia of their lives (those are usually the ones I "unsubscribe" to), but I do post little bits here and there that I think might make someone's day a bit better.

Lately, I've seen some of my friends advertise idiocy by perpetuating various hoaxes on Facebook, namely variations of privacy protection.  It's gotten extremely annoying to me because I can tell in an instant that it's a stupid hoax--I don't need to look it up to know that--but I'll look it up to get the reference that proves it.

One hoax, mentioning a "Graphic App", states that you need to uncheck posting options on a the poster, then add the comment "done" so that they will know to do the same to you (Snopes link: http://www.snopes.com/computer/facebook/graphapp.asp).  The stupidity of this is that doing this does NOTHING to protect your privacy, but will instead keep you and your friend from seeing posts each other make.  It will not stop any stranger from looking you up and browsing through any and all information that you have made PUBLIC on Facebook.

The other hoax I've lately seen is one that tries to use legalese and copyright to protect your private information and photos (Snopes link: http://www.snopes.com/computer/facebook/privacy.asp)--something that completely ignores the TERMS OF USE that YOU AGREED TO when you signed up to Facebook.  When I first saw this (there is a reference to the "Rome Statute" in it), I wanted to go right into the person's profile and SHARE everything in it--just to make a point.

My point in this is that if you are concerned about keeping your things private, then DON'T POST THEM WHERE THE WORLD CAN SEE IT!  You want to say something or post a picture that only your friends can see?  There is a setting in Facebook called "Friends".  If it says "Public", then the world can see it.  Change it to "Friends"--you have that control.  If you don't want even your friends to see it, then why are you putting it in Facebook in the first place?  What is wrong with you?

On Facebook, I posted some information I wish to keep private, but I've set it so that only my friends can see it.  They won't be able to share it--but I do trust them not to cut-and-paste it into a post of their own.  But even then--it wouldn't be the end of the world--the information I post IS public information, obtainable by other means (just a bit more difficult).  The things that really matter don't get posted anywhere, except where only I have access to.  Things like my social security number is safely hidden away, and you won't find it even if you get my password to my accounts.  But things like my photos--so what?  If I had any compromising photos of myself, you can rest assured I'd not be stupid enough to post them where anyone in the world could easily obtain them.  If you are concerned that someone might share and spread that naked photo of yourself--don't be stupid by publishing it.

Monday, March 4, 2013

WPF and MessageBox in Dispatcher.Invoke

Painful, painful, painful.

That was my experience when a WPF application I was creating would display a MessageBox on startup.  For efficiency many things were going on simultaneously, and most was tripped off with Dispatcher.BeginInvoke.  If something needed user interaction at startup, a MessageBox would display to get the user's attention.  The problem was, it was not modal, so the user would see the MessageBox flash and disappear, with the default value selected (which was very problematic).

A google search finally revealed the answer.  When using Dispatcher.Invoke, you need to pass the owning window to MessageBox:

MessageBox.Show(this, "message");

Friday, February 1, 2013

Helper for System.Diagnostics.Process

It's one thing to kick off a process from C# using Process.Start out of System.Diagnostics.  Nothing to that.  But what if you want to send the process some input beyond the arguments?  Or you want to perform processing on the output is sends to the console?



This code provides that help.

Just call ProcessHelper.Start.

The delegates for output and error provide a mechanism for processing each line of output.  The delegate will be called for each line produced.


This code below provides a simple example of how to use the ProcessHelper.  Each line of output is simply added to the StringBuilder objects.




StringBuilder outData = new StringBuilder();
StringBuilder errData = new StringBuilder();

System.Diagnostics.Process p = ProcessHelper.Start("MyProcess.exe", "arg1 arg2 arg3", "Standard Input Data",
        delegate(string parm)
        {
            outData.AppendLine(parm);
        },
        delegate(string parm)
        {
            errData.AppendLine(parm);
        }
);

p.WaitForExit();

Friday, May 18, 2012

ClickOnce Hell

This post is just a comment on my basic impression of ClickOnce, after having lived through developing for it for several years.

If your application is relatively small and simple, ClickOnce is great.  Basically, one (maybe two or three) project in the solution, compile size relatively small (to handle slow connections if necessary).

The problems start to arise when your solution starts to get complex.  Multiple projects, with references in XAML crossing the projects, and it gets a bit tricky.  Stuff that works fine in Visual Studio tends to not work once deployed through ClickOnce, unless you are very careful in your reference definitions.  It only takes one misdefined definition, and the whole thing can break--and the error messages received will likely be too generic to easily find it.

So with a complex application, ClickOnce becomes completely unfeasible.  In my opinion, it's a piece of crap, unless you keep your application small and simple--then it's great.

I use it on my small and simple applications as it offers easy deployment.

But on the one complex application I support, originally it was deployed as ClickOnce, but at some point the ClickOnce deployment broke, probably due to an incorrectly defined reference.  The application worked fine in Visual Studio, or when installed using WiX.  But it would go so far in ClickOnce, then blow up, and I never could figure out what ClickOnce didn't like.

So it was easier to give up on ClickOnce and switch to installing with WiX and use our own home-grown mechanism for updating the application.  That home-grown mechanism had its own set of problems, but at least it was under our control.

As a final note: WiX is great.  Being driven by XML allowed automatic update of the project definition through Team Systems Build.  If  I get around to it, I might post the recipe for this.  Leave a comment if this recipe would be valuable to you (to encourage me to post it sooner).

Assembly Unloading

I needed a simple way of checking version information of an assembly that was not loaded into memory.  On System.Reflection.Assembly, there is the Load method, but unfortunately, now the assembly is in memory with no way of unloading it--eating up precious RAM.

However, .NET offers the AppDomain.  Simply create a new AppDomain and load assemblies into that for reviewing information of the assembly, then unload the domain.

Example:


System.AppDomain newdomain = System.AppDomain.CreateDomain("DataVerification");
Assembly assm = newdomain.Load("MyAssembly.dll");
MessageBox.Show("Assembly version: " + assm.GetName().Version.ToString();
System.AppDomain.Unload(newdomain);

Tuesday, April 17, 2012

Perceived Types


Never could figure this one out complete.  We have an application that looks in the ClassesRoot Registry for the extension of a file, looking for the "PerceivedType" entry, and rendering an image if the PerceivedType is "image".  The problem for this particular computer was that its initial configuration was bad, and it had no extensions configured to image.  No idea why, nor how to automatically fix it or prevent it going forward.

Simple solution was to create a Registry merge file that included all extensions and added the "PerceivedType"="image" to all of them.  However, I stumbled onto a Windows Shell API call that would first check some hard-coded extensions for their perceived type, then check the registry, so there was potential that if our application used this call, AssocGetPerceivedType, the problem may not have surfaced in the first place.

But I could not find an example of the call defined in C# anywhere.  What a pain!  You mean no one has written C# that needs this call???????

I found C declaration for the call:

HRESULT AssocGetPerceivedType(
  __in       PCWSTR pszExt,
  __out      PERCEIVED *ptype,
  __out      PERCEIVEDFLAG *pflag,
  __out_opt  PWSTR *ppszType
    );

Now the question was, how to get it to C#?


It wasn't too hard, but did take some digging.  Below is the full code I used to get the perceived type of a file.  Simply check the returned value from the GetPerceivedType function below, passing the extension:






        [DllImport("Shlwapi", EntryPoint = "AssocGetPerceivedType")]
        private static extern int AssocGetPerceivedType(IntPtr extension, out IntPtr PERCEIVED, out IntPtr flag, out IntPtr PerceivedType);
        public string GetPerceivedType(string extension)
        {
            IntPtr result = IntPtr.Zero;
            IntPtr dummy = IntPtr.Zero;
            IntPtr intPtr_aux = Marshal.StringToHGlobalUni(extension);
            AssocGetPerceivedType(intPtr_aux, out dummy, out dummy, out result);
            string s = Marshal.PtrToStringAuto(result);
            return s;
        }
        

Tuesday, January 31, 2012

Axiom of Truth


If it takes two contractors six months to drive a project to failure, four contractors will achieve that failure in half the time, but it will take twice as long for the failure to be acknowledged.

Tuesday, January 10, 2012

Filter for all supported Image file types

Nifty little trick: for the OpenFileDialog, I want to set a filter for all Image file types supported by the PC, including ones that haven’t been invented yet. My code will load it into some sort of Image control for displaying. I could manually set my filter to something like “*.jpg;*.png”, etc., but then I’d have to update it whenever a new format was introduced, and some PCs may support types not supported by others, due to what has been installed on the PC.

This code below solves this problem.  It uses the MIME types that are configured for the PC, as defined in the Windows Registry.  Nice and simple solution.




  public static string GetImageFormatsFileFilter()
        {
            string retVal= "*" + string.Join(";*", SupportedImageFormats());
            return retVal;
        }
        public static string[] SupportedImageFormats()
        {
            List retVal = new List();
            RegistryKey BaseKey = Registry.ClassesRoot.OpenSubKey("MIME").OpenSubKey("Database").OpenSubKey("Content Type");
            foreach (string subkey in BaseKey.GetSubKeyNames())
            {
                if (subkey.ToUpperInvariant().StartsWith("IMAGE/"))
                {
                    string ext = (string)BaseKey.OpenSubKey(subkey).GetValue("Extension");
                    if (ext != null && !retVal.Contains(ext.ToUpperInvariant()))
                    {
                        retVal.Add(ext.ToUpperInvariant());
                    }
                }
            }
            return retVal.ToArray();
        }

Thursday, August 25, 2011

ListView column sorting

Found plenty of samples for WPF using a ListView and sorting on a column when the column header is clicked.
But I could not get any of them to work!!!
They never mentioned that this doesn't work with BindingList!!!
Switched to ObservableCollection<t>, and it worked fine.
I'm going to stop using that BindingList.

Russ

Wednesday, December 1, 2010

XAML and cryptic error message

Got the following cryptic error message when running a WPF application (a XamlParseException):
A 'Binding' cannot be used within a '<usercontrol>' collection. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
At first glance, everything looked good.  It might have been helpful if the error told me which property it was croaking on--but I guess MS can't learn to be specific with this XAML crap.  Fortunately, a quick google search and I stumbled onto a post where they identified thier problem when they got the same error: the owner in the DependencyProperty definition was not the control the property was on--changing it fixed their problem.  A second look at my code and I found I had done the same thing (the problem with cut-and-pasting).  Changing the owner type to the correct class fixed the problem.

Monday, November 29, 2010

Useless SQL Server Errors

Okay, there's nothing more useless than getting an error that doesn't tell you anything about how to fix it or, at the very least, why it occurred.
I had a Transaction Log backup job that was running regularly on SQL Server 2000, and was regularly getting failure, with nothing more than "SQLSTATE 42000, Error 22029".   Googling was not very helpful--seemed this could be caused by a who slew of possibilities, none of which appeared to apply to me.
It wasn't until I opened the text log file that was produced from the job and googled on the specific message ("Backup can not be performed on database <database>. This sub task is ignored.") that I found my answer, at http://support.microsoft.com/kb/303229.  It would have been nice to have something basic in the error message that told me the problem--it would have saved me a few hours of digging.
Basically, the Recovery Model for this database was "Simple".  Apparently you can't back up the transaction log on databases defined as Simple Recovery Model.  So there are two simple choices:  1. Change the Recovery Model to something other than "Simple" ("Bulk-Logged" or "Full" are both valid for backing up the transaction log), or 2. Don't back up the transaction log.
Since, in my case, the database is replicated to another server, it seemed rather redundant to back up the transaction log.  So, I turned off this job altogether.  Problem solved.

Wednesday, October 27, 2010

XAML pages won't generate c# code behind on compile

You'd think Visual Studio would know that if a file ended in ".xaml", then the file.g.cs code that is supposed to be generated from the XAML on compile would always be generated.

Unfortunately for me, I had some significant refactoring to do.  I moved existing XAML files around into new projects, and found that I couldn't compile.  It took me awhile to figure out that the file.g.cs files weren't being generated.  Why they weren't didn't make sense, until a Google search turned up that the properties of these files must be set the Build Action to "page".  Apparently Visual Studio changed the Build Action to something else.  When set to "page", they generated the code correctly.

Tuesday, October 26, 2010

Referencing bitmap image file in XAML.

It was driving me nuts.  I wanted to put a bitmap file into a project as a resource and reference it from XAML in another project.  I Googled the web and followed all the directions, but nothing worked.  Everything always referred to a local bitmap or a XAML file.


I followed all the directions, but nothing worked.  Basically, this is what I had: Image.png was in Resource.csproj.  UI.csproj (in the same solution) had Page.xaml that needed to display Image.png.

I could get the solution to compile, but the image would never display.  I followed all instructions to the letter, but nothing worked.  I think they're all liers, out to get me.

I finally figured it out on my own.  What I figured out even goes against some explicit instruction from one of the sites.

First, in your resource project (Resource.csproj above), add a resource file (a file ending in ".resx"--I called mine "Images.resx").  Do not use the Resources that are part of the project properties, but instead add a separate resource file.

Now add your image to that resource file.  Select the image in the resources and on the properties, change the Persistence to "Embedded in .resx".

Finally, add the ImageSource in the Resources of your XAML. In my case, my example is:

<ImageSource x:key="MyImage">pack://application:,,,/Resource;component/Images/Image.png</ImageSource>

Note that the path to the image is the resource file I added.

[EDIT]
Looks like I spoke too soon.  I still had my image in a folder that matched the same path, located in the UI project ("/Images/Image.png").  When I deleted that image, it failed.  Well, when I figure it out--if I figure it out--I'll update this with the correct information.

Too bad no one else can give good information on this--I'd really like to know.

Tuesday, September 21, 2010

System.ArgumentException: Invalid postback or callback argument

I've been going nuts on this one, and still don't have a good answer.
Basically, one of our web servers, using identical code (ASP.NET 3.0) as the production server (which works fine), suddenly decided to give me a "System.ArgumentException: Invalid postback or callback argument" every time I'd attempt to log in at the web site.  This was our QA box, used for final testing just before pushing stuff into production.  A quick Google search seemed to give some basic information--I had to used the Page EnableEventValidation="true" (you'd be stupid to set it to false), so the error did make some kind of sense.  But I could not overcome the issue.  There was no binding in the code-behind, so suggestioins of using !IsPostBack were useless.  The StackTrace suggested the error occurred on a TextBox, but there were only two textboxes: one for the username and one for the password.  Our development box worked fine and the production box worked fine. So what was the problem?
I tried all sorts of things--switching to Firefox, rebooting the QA servers, even restoring all production code onto the QA servers, and NOTHING!  The error just would not go away.  I added some code into development to do "ClientScript.RegisterForEventValidation" on ALL visibile controls, but I knew this really couldn't have anything to do with it--and I really didn't want to push development code into QA at this time.  I even checked the certificates (all websites are running using HTTPS) and noticed the QA box was running with an expired certificate--so I updated it, but still nothing.
In desparation, I finally tried logging into the website from a different computer--and lo and behold, it worked.  Okay, must be my PC, then--so I went back to my PC and tried logging in--this time it also worked.
WTF?  Well, at least if it happens again, I know how to clear it up.  But boy, I'd certainly appreciate any comments that could explain what happened and how to ever keep it from happening again.

Thursday, June 24, 2010

Team Systems Project Update

Okay, it's been awhile.  Been busy.



I've done some additional tweaks to my standard Team Systems Build project and have learned some useful tips--which I'm putting here.  I've discovered a number of issues since my original posting, which I've learned how to deal with.



The full project template can be found here



Below, I'm going step-by-step through that project file to explain what's going on and what you can customize for your needs.  This project builds and deploys a Click-Once project, plus updates a Windows Service on a separate Windows server.  The Solution that instigated this used a WPF Click-Once project that connected to a WCF service that ran on a separate machine, both of which were in the same solution, but not referencing each other.  The build project also does not run tests. Finally, there are two different philosophies for dealing with required app.config customizations for different deployments: copying in a unique app.config file for each deployment, or modifying the source app.config file on the fly.  I explore both methods, but found that copying in a unique one is probably the better choice.  I've commented out the code to change app.config as it's not always needed.



I've also added code to a couple of custom tasks that are useful, including a pretty good FTP process (I sometimes need to use FTP to deploy), and a custom RegEx to get information out of the project file (there might have been a good existing one that worked, but I found it easier just to write a custom one).



The custom tasks can be found here.



First, there's your basic setup:



Your standard XML encoding tag:
<?xml version="1.0" encoding="utf-8"?>



Basic project file definition:

<!-- Team Build Project Template: for ClickOnce applications -->



<!-- NOTE: if the app.config file requires modification for deployment variations, go to "!!!" location, uncomment and

       alter as needed

       -->

<!-- DO NOT EDIT the project element - the ToolsVersion specified here does not prevent the solutions

     and projects in the SolutionToBuild item group from targeting other versions of the .NET framework.

     -->

<Project DefaultTargets="Deploy" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">



  <!-- Do not edit this -->




Custom Task imports (these are important for doing things that aren't a standard part of MSBuild):


  <Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />



 This one defines our custom tasks. Be sure to define the path to where you dropped the BuildCodeBehind.Targets file in the zip file linked to above.


  <Import Project="$(MSBuildExtensionsPath)\BuildCodeBehind\BuildCodeBehind.Targets" />






Below is a set of common tasks that you can get from http://sdctasks.codeplex.com/:


   <!-- Microsoft.Sdc.Common.tasks from http://sdctasks.codeplex.com/ -->

  <Import Project="$(MSBuildExtensionsPath)\Microsoft\Sdc\Microsoft.Sdc.Common.tasks" />





Below is a set of common tasks that you can get from http://msbuildtasks.tigris.org/:
  <!-- MSBuild.Community.Tasks from http://msbuildtasks.tigris.org/ -->

  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>








Basic Configuration:


  <ProjectExtensions>

    <!-- Team Foundation Build Version - DO NOT CHANGE -->

    <ProjectFileVersion>2</ProjectFileVersion>

  </ProjectExtensions>



  Now here's where I put the various things that change from project to project.  I put it all together so that it's easy to find:


  <!-- *************************************************************  -->

  <!-- Solution specific Modifiable stuff begins here -->

  <PropertyGroup>



This defines the folder where the Click-Once project can be found.  This is not the full path, but only the subfolder under the solution:
       <ClickOnceProjectFolder>projectFolder</ClickOnceProjectFolder>





This is your solution name:
       <SolutionName>solutionName</SolutionName>



And the relative folder for the solution:
    <SolutionFolder>solutionFolder</SolutionFolder>



This is where any referenced files can be found.  Your click-once landing page should be in here:
    <ReferencedFilesFolder>referencedFilesFolder</ReferencedFilesFolder>



Your Click-Once application name as it will appear on the Land Page.  It will be compiled with this name followed by .exe:  <ClickOnceApplicationName>applicationName</ClickOnceApplicationName>



This parameter defines the project where the code can be found in Team Systems Source Control:


       <SourceCodeProject>ProjectNameInTeamSystemsSourceControl</SourceCodeProject>





    <!-- Deployment-Dependent Properties -->







  <PurposeName>Purpose name for Landing page</PurposeName>





  <!-- Final drop locations -->




Be sure to change WebServerURL to the URL of your web server:
  <ClickOnceUrl>http://WebServerURL/$(ClickOnceApplicationName)/</ClickOnceUrl>





Two deployment servers are assumed (our deployment is on a cluster), but you can repeat as many time as needed--if more needed just cut-and-paste for the extras.  If only one needed, then comment out RemoteServer2 references:
    <RemoteServer1>DeploymentServer1</RemoteServer1>

    <RemoteServer2>DeploymentServer2</RemoteServer2>






The deployment location for deploying by copying over the network:
<RemoteTarget1>\\$(RemoteServer1)\RootShare\$(ClickOnceApplicationName)</RemoteTarget1>

  <RemoteTarget2>\\$(RemoteServer2)\RootShare\$(ClickOnceApplicationName)</RemoteTarget2>






If updating a service, this is the service name that will need stopped and started for updating:
    <RemoteService>ServiceNameToStopAndStart</RemoteService>



And the is the full path to the Remote Service to copy to:    <RemoteServiceFolder>PathToServiceApplicationFolder</RemoteServiceFolder>



  <!-- Use "Environment" property to assist with differentiating with code branches -->

  <Environment></Environment>





  <!-- Standard Properties: These show up on the Landing Page. -->

  <ClickOnceAppTitle>ApplicationTitleForLandingPage - For $(PurposeName)</ClickOnceAppTitle>

  <AppDescription>ApplicationDescriptionForLandingPage.</AppDescription>



  <!-- Standard Company-level Properties: For the Landing Page -->

  <SupportUrl>SupportURLForLandingPage</SupportUrl>

  <Company>CompanyNameForLandingPage</Company>






If Deploying by FTP, these parameters define FTP deployment:
<FTPDeployServerUsername>FTPUsernameToDeploymentServer</FTPDeployServerUsername>

<FTPDeployServerPassword>FTPPasswordToDeploymentServer</FTPDeployServerPassword>

<RemoteDISTFolder>FTPFolderOnDeploymentServer</RemoteDISTFolder>





  <!-- General, unchanging Properties -->

  <AppConfigFile>app.config</AppConfigFile>





  <ConfigDir>$(SolutionRoot)\$(SolutionFolder)$(Environment)\$(ClickOnceProjectFolder)\</ConfigDir>




This parameter defines the drop location on the build server for the Click-Once project:
  <LocalDropLocation>e:\$(SourceCodeProject)\$(SolutionFolder)$(Environment)\ClickOnceDeploy\</LocalDropLocation>




This parameter defines the drop location of your secondary project (the services project):
<WCFServicesDropLocation>e:\$(SourceCodeProject)\$(SolutionFolder)$(Environment)\WCFServices\</WCFServicesDropLocation>

   

   

  <ClickOnceApplicationUrl>$(ClickOnceUrl)$(ClickOnceApplicationName).application</ClickOnceApplicationUrl>



  <!-- SigningCert:  May need altered -->




This defines the signing certificate:
  <SigningCert>$(ClickOnceProjectFolder)_TemporaryKey.pfx</SigningCert>

   



Your secondary project (service) to build: <SecondaryProject>$(SolutionRoot)/$(SolutionName)$(Environment)/SecondaryProjectFolder/SecondaryProjectFile.csproj</SecondaryProject>

  </PropertyGroup>

  <ItemGroup>

    <!-- Alter Landing Page if source location is different. -->



The location to find the Landing Page file:
    <ClickOnceLandingPage Include="$(SolutionRoot)\$(SolutionName)$(Environment)\$(ReferencedFilesFolder)\LandingPageFile.htm"/>

  </ItemGroup>



  <!-- Solution specific Modifiable stuff ends here -->

  <!-- *************************************************************  -->





Below are standard Microsoft parameters:


  <PropertyGroup>



    <!--  TESTING

     Set this flag to enable/disable running tests as a post-compilation build step.

    -->

    <RunTest>false</RunTest>



    <!--  CODE ANALYSIS

     Set this property to enable/disable running code analysis. Valid values for this property are

     Default, Always and Never.



         Default - Perform code analysis as per the individual project settings

         Always  - Always perform code analysis irrespective of project settings

         Never   - Never perform code analysis irrespective of project settings

     -->

    <RunCodeAnalysis>Default</RunCodeAnalysis>



    <!-- Additional Properties -->



    <!--  WorkItemType

     The type of the work item created on a build failure.

     -->

    <WorkItemType>Bug</WorkItemType>



    <!--  WorkItemFieldValues

     Fields and values of the work item created on a build failure.

    

     Note: Use reference names for fields if you want the build to be resistant to field name

     changes. Reference names are language independent while friendly names are changed depending

     on the installed language. For example, "System.Reason" is the reference name for the "Reason"

     field.

     -->

    <WorkItemFieldValues>System.Reason=Build Failure;System.Description=Start the build using Team Build</WorkItemFieldValues>



    <!--  WorkItemTitle

     Title of the work item created on build failure.

     -->

    <WorkItemTitle>Build failure in build:</WorkItemTitle>



    <!--  DescriptionText

     History comment of the work item created on a build failure.

     -->

    <DescriptionText>This work item was created by Team Build on a build failure.</DescriptionText>



    <!--  BuildLogText

     Additional comment text for the work item created on a build failure.

     -->

    <BuildlogText>The build log file is at:</BuildlogText>



    <!--  ErrorWarningLogText

     Additional comment text for the work item created on a build failure.

     This text will only be added if there were errors or warnings.

     -->

    <ErrorWarningLogText>The errors/warnings log file is at:</ErrorWarningLogText>



    <!--  UpdateAssociatedWorkItems

     Set this flag to enable/disable updating associated workitems on a successful build.

     -->

    <UpdateAssociatedWorkItems>true</UpdateAssociatedWorkItems>



    <!--  AdditionalVCOverrides

     Additional text for the VCOverrides file generated for VC++ projects.

     -->

    <AdditionalVCOverrides></AdditionalVCOverrides>



    <!--  CustomPropertiesForClean

     Custom properties to pass to the MSBuild task while calling the "Clean" target for all solutions.

     The format should be: PropertyName1=value1;PropertyName2=value2;...

     -->

    <CustomPropertiesForClean></CustomPropertiesForClean>



    <!--  CustomPropertiesForBuild

     Custom properties to pass to the MSBuild task while calling the default targets for all solutions.

     The format should be: Property1=value1;Property2=value2;...  To pass custom properties to

     individual solutions, use the Properties metadata item of the SolutionToBuild ItemGroup.

     -->

    <CustomPropertiesForBuild></CustomPropertiesForBuild>



  </PropertyGroup>



  <ItemGroup>

    <!--  SOLUTIONS

     The paths of the solutions to build. Paths can be server paths or local paths, but server paths

     relative to the location of this file are highly recommended.  To add/delete solutions, edit this

     ItemGroup. For example, to add a solution MySolution.sln, add the following line:

        

         <SolutionToBuild Include="$(BuildProjectFolderPath)/path/MySolution.sln" />



     To change the order in which the solutions are built, modify the order in which the solutions

     appear below.

    

     To call a target (or targets) other than the default, add a metadata item named Targets.  To pass

     custom properties to the solution, add a metadata item named Properties.  For example, to call

     the targets MyCustomTarget1 and MyCustomTarget2, passing in properties Property1 and Property2,

     add the following:

        

         <SolutionToBuild Include="$(BuildProjectFolderPath)/path/MySolution.sln">

             <Targets>MyCustomTarget1;MyCustomTarget2</Targets>

             <Properties>Property1=Value1;Property2=Value2</Properties>

         </SolutionToBuild>

    -->



    <SolutionToBuild Include="$(SolutionRoot)/$(SolutionName)$(Environment)/$(SolutionName).sln">

      <Targets></Targets>

      <Properties></Properties>

    </SolutionToBuild>



    <SolutionToPublish Include="$(SolutionToBuild)" />

  </ItemGroup>



  <ItemGroup>

    <!--  CONFIGURATIONS

     The list of configurations to build. To add/delete configurations, edit this value. For example,

     to add a new configuration, add the following lines:

        

         <ConfigurationToBuild Include="Debug|x86">

             <FlavorToBuild>Debug</FlavorToBuild>

             <PlatformToBuild>x86</PlatformToBuild>

         </ConfigurationToBuild>



     The Include attribute value should be unique for each ConfigurationToBuild node.

    -->

    <ConfigurationToBuild Include="Release|Any CPU">

      <FlavorToBuild>Release</FlavorToBuild>

      <PlatformToBuild>Any CPU</PlatformToBuild>

    </ConfigurationToBuild>



  </ItemGroup>



  <ItemGroup>

    <!--  TEST ARGUMENTS

     If the RunTest property is set to true then the following test arguments will be used to run

     tests. Tests can be run by specifying one or more test lists and/or one or more test containers.

    

     To run tests using test lists, add MetaDataFile items and associated TestLists here.  Paths can

     be server paths or local paths, but server paths relative to the location of this file are highly

     recommended:

    

        <MetaDataFile Include="$(BuildProjectFolderPath)/HelloWorld/HelloWorld.vsmdi">

            <TestList>BVT1;BVT2</TestList>

        </MetaDataFile>



     To run tests using test containers, add TestContainer items here:

    

        <TestContainer Include="$(OutDir)\HelloWorldTests.dll" />

        <TestContainer Include="$(SolutionRoot)\TestProject\WebTest1.webtest" />

        <TestContainer Include="$(SolutionRoot)\TestProject\LoadTest1.loadtest" />



     Use %2a instead of * and %3f instead of ? to prevent expansion before test assemblies are built

    -->



  </ItemGroup>



  <PropertyGroup>

    <!-- TEST ARGUMENTS

     If the RunTest property is set to true, then particular tests within a

     metadata file or test container may be specified here.  This is

     equivalent to the /test switch on mstest.exe.



     <TestNames>BVT;HighPriority</TestNames>

    -->



  </PropertyGroup>

  <ItemGroup>



    <!--  ADDITIONAL REFERENCE PATH

     The list of additional reference paths to use while resolving references. For example:

    

         <AdditionalReferencePath Include="C:\MyFolder\" />

         <AdditionalReferencePath Include="C:\MyFolder2\" />

    -->

  </ItemGroup>



These are the base files required for Click-Once:

  <ItemGroup>

    <ClickOnceInstallationFiles Include="$(LocalDropLocation)$(ClickOnceApplicationName).application"/>

    <ClickOnceInstallationFiles Include="$(LocalDropLocation)$(ClickOnceApplicationName).exe.manifest"/>

    <ClickOnceInstallationFiles Include="$(LocalDropLocation)setup.exe"/>

    <ClickOnceInstallationFiles Include="$(LocalDropLocation)default.htm"/>



  </ItemGroup>



  <Target Name="AfterGet">



    <Message Text="  ~~~ Doing After Get"/>



Here's where you set up custom app.config files based on deployment:

    <!-- *************************************************************  -->

       <!-- Custom Config file for differing deployments.  Option 1: modify on fly -->

    <!-- !!! Edit Config File here.-->

   

    <!--

    <Message Text="  ~~~ Modifying Config file: AppConfigFile=$(ConfigDir)app.config"/>

    -->



    <!-- ModifyFile is out of Microsoft.Sdc.Common.tasks -->

    <!--

    <ModifyFile

        Path="$(ConfigDir)$(AppConfigFile)"

        XPath="/configuration/userSettings/$(ClickOnceApplicationName).Properties.Settings/setting[@name='SettingToModify']/value"

     NewValue="$(NewSetting)"

     Force="true" />

    -->



    <!-- End Config File modification here  -->

       <!-- Custom Config file for differing deployments.  Option 2 (better): copy unique in from source -->

<!--  !!! Edit Config File here.  -->

<!--

    <Message Text="  ~~~ Deleting app.config file"/>

    <File.DeleteFiles

            Files="$(ConfigDir)app.config"

            TreatErrorsAsWarnings="false"

            Force="true" />



    <Message Text="  ~~~ Copying in Config file from $(CUCustomStuff)$(AppConfigFile)"/>



    <Copy SourceFiles="$(CUCustomStuff)$(AppConfigFile)" DestinationFiles="$(ConfigDir)app.config"/>



-->



    <!--  End Config File modification here    -->

    <!-- *************************************************************  -->







Now we get the latest changeset out of source control to use as our "Revision" number:

    <!-- Getting version data -->

    <!-- TfsVersion is out of  MSBuild.Community.Tasks -->

    <TfsVersion LocalPath="$(SolutionRoot)">

      <Output TaskParameter="Changeset" PropertyName="Revision" />

    </TfsVersion>



    <Message Text="  ~~~ Revision is: $(Revision)" />





    <!--  Setting the Application (Publish) version for ClickOnce by modifying the C# Project file. -->

    <CreateProperty Value="\&lt;ApplicationRevision\&gt;[0-9]+\&lt;\/ApplicationRevision\&gt;">

      <Output PropertyName="RegExMatch" TaskParameter="Value" />

    </CreateProperty>

    <CreateProperty Value="&lt;ApplicationRevision&gt;$(Revision)&lt;/ApplicationRevision&gt;">

      <Output PropertyName="RegExReplace" TaskParameter="Value" />

    </CreateProperty>



    <!-- RegEx is out of Microsoft.Sdc.Common.tasks -->

    <Regex Path="$(ConfigDir)$(ClickOnceApplicationName).csproj"

            RegularExpression="$(RegExMatch)"

            NewValue="$(RegExReplace)"

            Force="true"/>



    <Message Text="  ~~~ Getting ApplicationVersion for $(ConfigDir)$(ClickOnceApplicationName).csproj" />







    <!-- ReadLinesFromFile is out of Microsoft.Sdc.Common.tasks -->

    <ReadLinesFromFile File="$(ConfigDir)$(ClickOnceApplicationName).csproj">

      <Output TaskParameter="Lines"

      ItemName="ItemsFromFile" />

    </ReadLinesFromFile>





    <CreateProperty Value="@(ItemsFromFile)">

      <Output PropertyName="wrkItemsFromFile" TaskParameter="Value" />

    </CreateProperty>



    <!-- RegexCapture is a custom task, out of the RCO BuildCodeBehind.dll -->

    <RegexCapture Input="@(ItemsFromFile)" Expression="\&lt;ApplicationVersion\&gt;[0-9]+\.[0-9]+\.[0-9]+">

      <Output ItemName="VersionInfo" TaskParameter="Output"></Output>

    </RegexCapture>



    <CreateProperty Value="@(VersionInfo)">

      <Output PropertyName="VersionQuestion" TaskParameter="Value" />

    </CreateProperty>





    <!-- RegexCapture is a custom task, out of the RCO BuildCodeBehind.dll -->

    <RegexCapture Input="@(VersionInfo)" Expression="[0-9]+\.[0-9]+\.[0-9]+">

      <Output ItemName="VersionInfo2" TaskParameter="Output"></Output>

    </RegexCapture>



    <CreateProperty Value="@(VersionInfo2)">

      <Output PropertyName="VersionData" TaskParameter="Value" />

    </CreateProperty>



    <Message Text="  ~~~ Publish Version = $(VersionData).$(Revision)" />



    <!-- end getting version data -->



By setting the "Publish" version, an update can now be forced without having to manually change the publish version in the project.



  </Target>





  <Target Name="AfterCompile">

    <Message Text="  ~~~ BuildProjectFolderPath=$(BuildProjectFolderPath)"/>

    <Message Text="  ~~~ SolutionRoot=$(SolutionRoot)"/>





    <Message Text="  ~~~ copying @(ClickOnceLandingPage) to $(LocalDropLocation)\default.htm"/>

    <Copy  SourceFiles="@(ClickOnceLandingPage)"

            DestinationFiles="$(LocalDropLocation)\default.htm"/>



    <Message Text="  ~~~ Modifying landing page for app name, title, and version"/>



    <!-- RegEx is out of Microsoft.Sdc.Common.tasks -->

    <Regex

          Path="$(LocalDropLocation)\default.htm"

          RegularExpression="#APPLICATION_NAME#"

          NewValue="$(SolutionName)"

          Force="true"/>



    <!-- RegEx is out of Microsoft.Sdc.Common.tasks -->

    <Regex

        Path="$(LocalDropLocation)\default.htm"

        RegularExpression="#TITLE#"

        NewValue="$(ClickOnceAppTitle)"

        Force="true"/>





    <!-- RegEx is out of Microsoft.Sdc.Common.tasks -->

    <Regex

        Path="$(LocalDropLocation)\default.htm"

        RegularExpression="#VERSION#"

        NewValue="$(VersionData).$(Revision)"

        Force="true"/>



    <!-- RegEx is out of Microsoft.Sdc.Common.tasks -->

    <Regex

        Path="$(LocalDropLocation)\default.htm"

        RegularExpression="#APPDESCRIPTION#"

        NewValue="$(AppDescription)"

        Force="true"/>



    <Message Text="  ~~~ Building $(VersionData).$(Revision)"/>





Build the Click-Once project:



    <MSBuild

      Projects="$(SolutionRoot)/$(SolutionName)$(Environment)/$(ClickOnceProjectFolder)/$(ClickOnceApplicationName).csproj"

      Properties="Configuration=%(ConfigurationToBuild.FlavorToBuild);PublishDir=$(LocalDropLocation);OutDir=$(LocalDropLocation);ApplicationVersion=$(VersionData).$(Revision);Satellite_FileVersion=$(VersionData).$(Revision);Satellite_ProductVersion=$(VersionData).$(Revision);Satellite_Version=$(VersionData).$(Revision);ApplicationRevision=$(Revision);"

      Targets="Publish" />



Build your secondary (service) project:

    <MSBuild

      Projects="$(SecondaryProject)"

      Properties="Configuration=%(ConfigurationToBuild.FlavorToBuild);PublishDir=$(WCFServicesDropLocation);OutDir=$(WCFServicesDropLocation);ApplicationVersion=$(VersionData).$(Revision);Satellite_FileVersion=$(VersionData).$(Revision);Satellite_ProductVersion=$(VersionData).$(Revision);Satellite_Version=$(VersionData).$(Revision);ApplicationRevision=$(Revision);"

      Targets="Build" />



  </Target>







  <Target Name="AfterDropBuild">



    <Message Text="Deploying files..." />





    <CreateItem Include="$(VersionData).$(Revision)">

      <Output TaskParameter="Include"

              ItemName="TestGroup"/>

    </CreateItem>





    <!-- RegexReplace is out of  MSBuild.Community.Tasks -->

    <RegexReplace Input="@(TestGroup)" Expression="\." Replacement="_" Count="-1">

      <Output PropertyName ="BuildNumberFolder" TaskParameter="Output" />

    </RegexReplace>







    <Message Text="  ~~~ BuildNumberFolder = $(BuildNumberFolder)" />



    <CreateItem

                      Include="$(LocalDropLocation)Application Files\$(ClickOnceApplicationName)_$(BuildNumberFolder)\*.*">

      <Output

                        TaskParameter ="Include"

                        ItemName ="DroppedApplicationItems"/>

    </CreateItem>



    <CreateItem

         Include="$(WCFServicesDropLocation)\*.*">

      <Output

                        TaskParameter ="Include"

                        ItemName ="DroppedWCFItems"/>

    </CreateItem>

   

   

    <CreateProperty Value="@(DroppedApplicationItems)">

      <Output PropertyName="ApplicationFiles" TaskParameter="Value" />

    </CreateProperty>





    <Message Text="  ~~~ Looking for Application Files in: $(ApplicationFiles)" />

    <Message Text="  ~~~ ApplicationFiles=$(ApplicationFiles)" />







    <Message Text="  ~~~ Copying ClickOnceInstallation files to $(RemoteTarget1)" />



    <Copy

      SourceFiles="@(ClickOnceInstallationFiles)"

      DestinationFiles="@(ClickOnceInstallationFiles->'$(RemoteTarget1)\%(Filename)%(Extension)')"

      />







    <Message Text="  ~~~ Copying Application files to $(RemoteTarget1)" />

    <Copy

  SourceFiles="@(DroppedApplicationItems)"

  DestinationFiles="@(DroppedApplicationItems->'$(RemoteTarget1)\Application Files\$(ClickOnceApplicationName)_$(BuildNumberFolder)\%(Filename)%(Extension)')"

      />

    <Message Text="  ~~~ Copying ClickOnceInstallation files to $(RemoteTarget2)" />

    <Copy

   SourceFiles="@(ClickOnceInstallationFiles)"

   DestinationFiles="@(ClickOnceInstallationFiles->'$(RemoteTarget2)\%(Filename)%(Extension)')"

     

      />

    <Message Text="  ~~~ Copying Application files to $(RemoteTarget2)" />

    <Copy

  SourceFiles="@(DroppedApplicationItems)"

    DestinationFiles="@(DroppedApplicationItems->'$(RemoteTarget2)\Application Files\$(ClickOnceApplicationName)_$(BuildNumberFolder)\%(Filename)%(Extension)')"

      />











    <Message Text="~~~~Deploying WCF files...to $(RemoteServer1)" />

 <!-- ServiceController is out of  MSBuild.Community.Tasks -->

    <ServiceController MachineName="$(RemoteServer1)" Action="Stop" ServiceName="$(RemoteService)"></ServiceController>

    <Copy

      SourceFiles="@(DroppedWCFItems)"

      DestinationFiles="@(DroppedWCFItems->'\\$(RemoteServer1)\$(RemoteServiceFolder)\%(Filename)%(Extension)')"

     

      />



 <!-- ServiceController is out of  MSBuild.Community.Tasks -->

    <ServiceController MachineName="$(RemoteServer1)" Action="Start" ServiceName="$(RemoteService)"></ServiceController>

   

   

    <Message Text="~~~~Deploying WCF files...to $(RemoteServer2)" />

 <!-- ServiceController is out of  MSBuild.Community.Tasks -->

    <ServiceController MachineName="$(RemoteServer2)" Action="Stop" ServiceName="$(RemoteService)"></ServiceController>

    <Copy

      SourceFiles="@(DroppedWCFItems)"

      DestinationFiles="@(DroppedWCFItems->'\\$(RemoteServer2)\$(RemoteServiceFolder)\%(Filename)%(Extension)')"

      />



 <!-- ServiceController is out of  MSBuild.Community.Tasks -->

    <ServiceController MachineName="$(RemoteServer2)" Action="Start" ServiceName="$(RemoteService)"></ServiceController>



 <!-- FTP to Target Starts here -->

<!--

    <Message Text="~~~ Uploading files " />

    <FTP

      LocalRootPath="e:\$(SourceCodeProject)\$(SolutionFolder)$(Environment)\"

      Task="upload"

      Files="@(ClickOnceInstallationFiles)"

      RemotePath="$(RemoteDISTFolder)"

      Host="$(RemoteServer1)" Username="$(FTPDeployServerUsername)" Password="$(FTPDeployServerPassword)" Binary="true" >



    </FTP>

-->

    <!-- FTP to target ends here -->



Clean up the build folders.  Once deployed, you generally don't need the stuff on the build machines.  So remove all unnecessary stuff to save on disk space.
    <!-- Clean up -->

    <Message Text="~~~ Deleting files (cleanup)" />



  

    <Exec ContinueOnError="true"

          Command="rmdir /s/q &quot;e:\$(SourceCodeProject)\$(SolutionFolder)$(Environment)&quot;" />

   

    <Exec ContinueOnError="true"

               Command="rmdir /s/q &quot;$(DropLocation)\$(BuildNumber)\Release&quot;" />

  </Target>





</Project>