Thursday, December 17, 2009
Detecting Design Mode in WPF controls
Tuesday, December 8, 2009
Updating WPF window from background thread
But you want to notify the user when the process is done. Trouble is, since the background process is running in a different thread from the user interface, the process crashes if you make any attempt to update the user interface directly.
Here's some sample code to solve this with the Dispatcher--kind of well known, but nice to document anyhow:
void UpdateText(string text)
{
if (this.Dispatcher != System.Windows.Threading.Dispatcher.CurrentDispatcher)
{
this.Dispatcher.Invoke(new Action
}
else
{
this.sampleTextBox.Text = text;
}
}
Monday, December 7, 2009
C# Inheritance, Typecasting, and Reflection
This proved a minor nuisance as the code had a simplistic work-around, but this might not always be the case.
Below is an example of what I'm talking about:
using System.Reflection;
public static class UtilityClass
{
public static object GetResult(object workClass)
{
string ResultingClassName = "Result" + workClass.GetType().ToString();
Type provider = Type.GetType(ClassName, true);
ConstructorInfo constructor = provider.GetConstructor(new Type[0]);
return constructor.Invoke(new object[0]);
}
}
public class ResultClass
{
public ResultClass()
{
}
}
public class Class
{
public ResultClass GetResult()
{
return (ResultClass)UtilityClass.GetResult(this);
}
}
public class ChildClass : Class
{
public void DoTest()
{
ResultClass x = ((Class)this).GetResult();
}
}
The above code will fail with an exception when DoTest() in ChildClass is called, because it will try to create ResultChildClass, which does not exist. No matter how you cast, you can't get this to work, because the GetResult method of UtilityClass, using Reflection, will always identify the object for what it really is.
In my case, the solution was to simply create a new "Class" object and copy the base of the ChildClass object into it, then call the "GetResult" method to get my "ResultClass".
Friday, December 4, 2009
System.OutOfMemoryException from Visual Studio
I found a decent work-around is to Build the solution first. Then run the solution. This two-step process seems to solve the issue in that when you run it, Visual Studio doesn't build it--so I guess it's got time to clean up memory before starting your executable.
You might eventually still run out of memory, but this'll give you some extra time before you have to close Visual Studio and start it up again.
Thursday, December 3, 2009
XAML Binding to Datasets
Datasets are great when the data is OneWay binding, because heirarchical data relationships are so easy to define. But you've got to get creative and use something else, such as a BindingList<> when the binding must be TwoWay or OneWayToSource.
Wednesday, December 2, 2009
XAML and the loss of Intellisense
Tuesday, December 1, 2009
Using Syncfusion
Friday, November 27, 2009
Syncfusion GridDataControl and referencing other columns
WPF form and debugging data
class
LogObjectDataConverter : IValueConverter{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value;
}
Thursday, November 19, 2009
0x8007001f from ClickOnce install/upgrade
- Exception occurred loading manifest from file <dllfilename>.dll: the manifest may not be valid or the file could not be opened.
- Parsing and DOM creation of the manifest resulted in error. Following parsing errors were noticed:
- A device attached to the system is not functioning. (Exception from HRESULT: 0x8007001F)
Wednesday, October 28, 2009
Why Team Build won't customize app.config
Wednesday, September 30, 2009
WIA in C# Redux: for Vista!
///
/// This is a simple wrapper around WIA.
///
public class Scanner
{
public class AcquireEventArgs : EventArgs
{
public Image Image { get; internal set; }
}
WIA.DeviceManager manager = new WIA.DeviceManagerClass();
///
/// Acquires the images.
///
public void AcquireImages()
{
WIA.CommonDialogClass diag = new WIA.CommonDialogClass();
System.Object Object1 = null;
System.Object Object2 = null;
WIA.Device dev = null;
try
{
dev = diag.ShowSelectDevice(WIA.WiaDeviceType.UnspecifiedDeviceType, true, false);
}
catch (Exception ex)
{
if (ex.Message.EndsWith("00153.") || ex.Message.EndsWith("21006A."))
{
throw new ScannerException("Scanner Not connected", ex);
}
else
{
throw new ScannerException("Scanner problem", ex);
}
}
if (dev != null)
{
WIA.Item Item1 = ItemObjectReturnedFromInitializingScanner(ref dev);
WIA.ImageFile Image1 = new WIA.ImageFile();
WIA.ImageProcess ImageProcess1 = new WIA.ImageProcess();
Object1 = (Object)"Convert";
ImageProcess1.Filters.Add(ImageProcess1.FilterInfos.get_Item(ref Object1).FilterID, 0);
Object1 = (Object)"FormatID";
Object2 = (Object)WIA.FormatID.wiaFormatTIFF;
ImageProcess1.Filters[1].Properties.get_Item(ref Object1).set_Value(ref Object2);
Object1 = (Object)"Compression";
Object2 = (Object)"CCITT4";
ImageProcess1.Filters[1].Properties.get_Item(ref Object1).set_Value(ref Object2);
Object1 = null;
Object2 = null;
try
{
WIA.ImageFile imagefile = Item1.Transfer(WIA.FormatID.wiaFormatTIFF) as WIA.ImageFile;
if (ImageScanned != null)
{
AcquireEventArgs e = new AcquireEventArgs();
using (MemoryStream ms = new MemoryStream((byte[])imagefile.FileData.get_BinaryData()))
{
e.Image = new Bitmap(ms);
}
ImageScanned(this, e);
}
}
catch (Exception ex)
{
throw new ScannerException("Problem with Scanner", ex);
}
}
}
private WIA.Item ItemObjectReturnedFromInitializingScanner(ref WIA.Device Scanner)
{
WIA.Item Item1 = null;
Object Object1 = null;
Object Object2 = null;
Int32 DPI = 200;
foreach (WIA.Item CurrentItem in Scanner.Items) // 'Scanner settings.
{
Item1 = CurrentItem;
try
{
Object1 = (Object)"6146";
Object2 = (Object)4;
CurrentItem.Properties.get_Item(ref Object1).set_Value(ref Object2);
}
catch
{ }
try
{
Object1 = (Object)"6147";
Object2 = (Object)DPI;
CurrentItem.Properties.get_Item(ref Object1).set_Value(ref Object2);
}
catch
{ }
try
{
Object1 = (Object)"6148";
Object2 = (Object)DPI;
CurrentItem.Properties.get_Item(ref Object1).set_Value(ref Object2);
}
catch
{ }
try
{
Object1 = (Object)"6149";
Object2 = (Object)0;
CurrentItem.Properties.get_Item(ref Object1).set_Value(ref Object2);
}
catch
{ }
try
{
Object1 = (Object)"6150";
Object2 = (Object)0;
CurrentItem.Properties.get_Item(ref Object1).set_Value(ref Object2);
}
catch
{ }
try
{
Object1 = (Object)"6151";
Object2 = (Object)(8.5 * DPI);
CurrentItem.Properties.get_Item(ref Object1).set_Value(ref Object2);
}
catch
{ }
try
{
Object1 = (Object)"6152";
Object2 = (Object)(11.5 * DPI);
CurrentItem.Properties.get_Item(ref Object1).set_Value(ref Object2);
}
catch
{ }
foreach (WIA.Property Prop in Scanner.Properties)
{
if (Prop.PropertyID == 3088) //
{
try
{
Object1 = (Object)5;
Prop.set_Value(ref Object1); //'This is my effort to enforce duplex. I
}
catch (Exception) { }
}
}
}
Object1 = null;
Object2 = null;
return Item1;
}
///
/// Occurs when [image scanned].
///
public event EventHandler
}
Monday, September 21, 2009
Mouse drawing in WPF
<
UserControl x:Class="RCO.Imaging.SignaturePad.MouseSignaturePad.SignaturePad"xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
HorizontalAlignment="Stretch" Width="Auto" Height="Auto" VerticalAlignment="Stretch" > <InkCanvas Name="canvas1" Width="Auto" Height="Auto" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" ></InkCanvas>
</
UserControl>And here is the C# behind it:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace
MousePad{
/// <summary>
/// Interaction logic for SignaturePad.xaml
/// </summary>
public partial class SignaturePad : UserControl
{
public SignaturePad()
{
InitializeComponent();
} public static RenderTargetBitmap ToImageSource(FrameworkElement obj)
{ // Save current canvas transform
Transform transform = obj.LayoutTransform;
obj.LayoutTransform = null;
// fix margin offset as well
Thickness margin = obj.Margin;
obj.Margin = new Thickness(0, 0,
margIn.Right - margin.Left, margin.Bottom - margin.Top);
// Get the size of canvas
Size size = new Size(obj.ActualWidth, obj.ActualHeight);
// force control to Update
obj.Measure(size);
obj.Arrange(new Rect(size));
RenderTargetBitmap bmp = new RenderTargetBitmap(
(int)obj.ActualWidth, (int)obj.ActualHeight, 96, 96, PixelFormats.Pbgra32);
bmp.Render(obj);
// return values as they were before
obj.LayoutTransform = transform;
obj.Margin = margin;
return bmp;
}
public void AcceptImage()
{
using (System.IO.MemoryStream outStream = new System.IO.MemoryStream())
{
// Use png encoder for our data
PngBitmapEncoder encoder = new PngBitmapEncoder();
// push the rendered bitmap to it
encoder.Frames.Add(BitmapFrame.Create(ToImageSource(canvas1)));
// save the data to the stream
encoder.Save(outStream);
outStream.Position = 0;
// Do something with the MemoryStream here--save it to file or pass it back into system.
}
}
}
}
Thursday, September 17, 2009
WIA in C#
using
System;using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using WIALib;
namespace WIAWrapper
{
/// <summary>
/// This is a simple wrapper around WIA.
/// </summary>
public class Scanner
{
public class AcquireEventArgs : EventArgs
{
public Image Image { get; internal set; }
}
object selectUsingUI = System.Reflection.Missing.Value;
ItemClass wiaRoot;
WIALib.WiaClass wiaManager;
public Scanner()
{
wiaManager = new WIALib.WiaClass();
wiaRoot = (ItemClass)wiaManager.Create(ref selectUsingUI);
}
/// <summary>
/// Acquires the images.
/// </summary>
public void AcquireImages()
{
List<Image> retVal = new List<Image>();
CollectionClass wiaPics = wiaRoot.GetItemsFromUI(WiaFlag.SingleImage, WiaIntent.ImageTypeColor) as CollectionClass;
wiaManager.OnTransferComplete += new _IWiaEvents_OnTransferCompleteEventHandler(wiaManager_OnTransferComplete);
foreach (object wiaObj in wiaPics)
{
ItemClass wiaItem = (ItemClass)Marshal.CreateWrapperOfType(wiaObj, typeof(ItemClass));
string imgFile = Path.GetTempFileName();
wiaItem.Transfer(imgFile, true);
}
} public event EventHandler<AcquireEventArgs> ImageScanned; /// <summary>
/// Wias the manager_ on transfer complete.
/// </summary>
/// <param name="Item">The item.</param>
/// <param name="Path">The path.</param>
void wiaManager_OnTransferComplete(Item Item, string Path)
{
if (ImageScanned != null)
{
AcquireEventArgs e = new AcquireEventArgs();
e.Image = new Bitmap(Path);
ImageScanned(this, e);
}
}
}
}
Wednesday, July 15, 2009
Using XPath to modify app.config on setup
Sample code to modify the app.config when executing a setup package:
This assumes it is in the "Committed" event of a projectinstaller class. This sample only modifies a connectionString, but can easily be used for anything:
string
assemblypath = Context.Parameters["assemblypath"];string appConfigPath = assemblyPath + ".config";
XmlDocument doc = new XmlDocument();
doc.Load(appConfigPath);
XmlNode aConnection = doc.SelectSingleNode("/configuration/connectionStrings/add[@name=\"aConnectionIdentifier\"]");
aConnection.Attributes["connectionString"].Value = "Data Source=.;Initial Catalog=MyDatabase;User Id=MyUser;Password=MyPassword;";
doc.Save(appConfigPath);
Wednesday, July 8, 2009
Embedding your DLLs into your EXE
Good info at http://dotnetzip.codeplex.com/Wiki/View.aspx?title=Embed%20DotNetZip&referringTitle=Home for embedding your DLLs into your EXE, instead of having them separate. Not sure why you might want to do this, other than to obfusicate your code a bit, but you never know.
Great .NET Zip library
Wednesday, July 1, 2009
ClickOnce and Team Build
Okay, I've figured it all out. First off, there are too many "Version" terms. There's the AssemblyVersion, the version the user sees on the landing page, the BuildNumber, which is commonly part of the "Version", and finally, the one thing my brain was not wrapping itself around, the "PublishVersion". This publish version is not visible to the end-user, but it's stored in the manifest and is key to letting the ClickOnce application know that an update is available. I was wanting to override it, but I wasn't fully grasping that it was really its own thing--separate from AssemblyVersion.
What I wanted was a PublishVersion that came from the Publish version on the properties page of the project, but used the ChangeSet in place of the Revision number. I didn't care if the Revision was updated in Source Control since it would always be ignored anyhow and overridden on each build.
Everything I found went through all sorts of hoops to basically accomplish this. Apparently MSBuild doesn't offer a simple way to pass this as a property on the task, so for awhile it appeared the only solution was to use MAGE somehow--but I wanted to avoid that because ther seemed to be such an overly-complicated set of configuration for it--it would also have to be used to generate the manifest
Then it occurred to me--the light bulb upstairs finally came on--why not just update the csproj file using a XMLModify to replace the Revision for the Publish version with the changeset. I could do this just like I modify the app.config file--in the AfterGet target override. It would be so simple and elegant--just one simple task to add to what I already had!
Well, for some reason I couldn't get Xml.ModifyFile to work on the csproj file, so I just switched over to File.Regex.
I also did find I had to make a minor tweak of the Regex that extracted the version. My Regex only worked if the Major, Minor, and build numbers were single digits. I just had to add a "+" after each bracket ("]") for it to work.
Finally, I had modified the Copy tasks that deployed the files to their deployment site so that only the necessary files would be copied, instead of every file (which originally included all DLLs and EXEs--I only needed the .DEPLOY and related files).
The template for this Team Build project can be found at http://docs.google.com/View?id=ddzhk5g7_2dgt2sxdj
I've uploaded the C# source code for the custom RegexCapture task to http://docs.google.com/View?id=ddzhk5g7_3gqtnz9fs
Friday, June 26, 2009
Ico to PNG conversion
Originally had http://www.icopng.com/index.php, but that seems to be offline, now.
Image editor to extend MS Paint (transparency & animation)
TabControl and the Appearance property
How insane. I had set the Appearance property of the Tabcontrol to "Buttons" and thought, "That looks cool!" I also set the Alignment to "Bottom". Something different and pretty slick-looking.
But then I spent the next few hours trying to figure out why my custom control that I dropped on one of these tab pages wouldn't render. I kept thinking there was something wrong with the custom control. Finally, after a Google, http://msdn.microsoft.com/en-us/library/system.windows.forms.tabcontrol.appearance(VS.71).aspx explained it all.
Selecting "Buttons" or "FlatButtons" for the TabControl's Appearance property requires that the Alignment be "Top". Of course, the notation just says that the alignment must be "Top" or the "Buttons" won't display correctly. It doesn't say anything about any other controls, which is what I experienced.
Alignment to "Top" if altering the Appearance on the TabControl to Buttons or FlatButtons.
Wednesday, June 24, 2009
Finding a control within a control
I found Control.ControlCollection.Find, but the Help for it (http://msdn.microsoft.com/en-us/library/system.windows.forms.control.controlcollection.find.aspx) did not give any examples using a wildcard matching. Yes, I could iterate through all controls, but that seems inefficient. Yes, I could use LINQ, but this project has the limitation of being required to be .NET 2.0, not .NET 3.5.
So, time for the sandbox. Simple test: I create a form, with a groupbox (groupBox1) and put three controls in it. One Button control named "button1" and two CheckBox controls named "checkBox1" and "checkBox2". Then ran this code:
Control
[] ctl = groupBox1.Controls.Find("checkBox*", true);
Control[] ctl1 = groupBox1.Controls.Find("checkBox1", true);
Control[] ctl2 = groupBox1.Controls.Find("checkBox2", true);
ctl had no elements, while ctl1 and ctl2 both had one element (due to the exact match). I also tried changing "checkBox*" to just "checkBox" with the same result.
Well, this was rather annoying. After all, what's the point of returning an array, if the best you're going to do is return one item?
Then, on an whim I tried the following code:
this
.checkBox1.Name = "test";
this.checkBox2.Name = "test";
Control[] ctl = groupBox1.Controls.Find("test", false);And good news! I got two items back!
So the trick in my case was when I go to generate my dynamic control, I always set the "Name" property the same. Then Controls.Find will find all matches, since you cannot wildcard it.
ClickOnce and Team Systems Summary
1. Log into the build machine as the tfsbuild user.
2. Open the solution from source control in Visual Studio and publish. This sets up the ClickOnce publish key. This step can be circumvented with the use of MAGE or MAGEUI, but who wants to go through all that when this works.
3. Copy a Team Build project from a previous ClickOnce build project (or use some kind of template, or something).
- Set up your landing page
- If the ClickOnce project references a separate project within the solution, be sure to define both the SolutionToBuild and SolutionToPublish tags. (Otherwise, just the SolutionToPublish is needed).
- Define the Application Title, Support URL, Company, ClickOnceURL, and any other application and deployment-specific properties.
- Modify App.config settings in the AfterGet target override. Also, obtain Version definition in the AfterGet.
- In the AfterDropBuild target override, copy all files from the drop folder to the deployment folder(s).
This pretty much summarizes what works for me.
I promised to go through each tag to explain everything. I did stumple onto http://msdn.microsoft.com/en-us/magazine/dd458792.aspx today, which appears to be a terrific tutorial. I wish I had found it a few weeks (or even months) ago. Because of how good this link is, I'm going to supply only the Cliff Notes version.
In detail, the meaning of each tag in a Team Systems Build project:
<Project
Defines the project.
Attributes:
DefaultTargets - Ignored by Team Systems 2008. This is only used by MSBuild if run directly. Team Systems Build always uses only EndToEndIteration. This attribute defines which target MSBuild should use as its main entry point.
<Import
Specifies include projects files, which are used to define extension tasks.
Attributes:
Project - Defines the project file to import.
<RunCodeAnalysis
Defines whether or not the build will run Code Analysis.
<RunTest
<MetaDataFile
<TestList
Enables/disables the running of tests. Personally, I think this is a great feature to have, but unfortunately, it's often quite difficult to put together adequate tests in a system that integrates with user interaction and separate systems, such as the OpenVMS systems my shop uses.
<Message
Adds a message to the build log
Attributes:
Text - the message.
<ItemGroup
<PropertyGroup
<SolutionToBuild
Defines the solution file that should be compiled.
<SolutionToPublish
Defines the solution file that is to be published.
<ConfigurationToBuild
Defines whether the soution should be built for Debug or Release and for what kind of CPU.
<Target
Overridable point that MSBuild runs.
Attributes:
Name - the name of the target.
<UsingTask
Defines a custom task.
Attributes:
Assembly File - Defines the dll that includes the custom task.
TaskName - Defines the task name, which must be qualified with the full namespace.
<Custom tag
Any custom task defined in a DLL that is specified in a <UsingTask tag. I'll go into detail in a future post as to how to create your own custom tasks.
<Copy
Task to copy file(s)
<CreateProperty
Creates a property
<CreateItem
Creates an ItemList.
http://msdn.microsoft.com/en-us/magazine/dd150090.aspx is a good article on Team Build 2008. If you haven't already figured it out, I'm using Team Build 2008.
This pretty much covers all I care about.
TODO: detail about custom tasks.
TODO: supply nice template Build Projects--One for a ClickOnce application, and one for a web application. I've still got a bit of refining on my current project file in that my task to copy the files from the drop folder to the web cluster for final deployment copies too much and clutters things up (it doesn't really hurt anything, but it's really not right). So I've got to take a little to configure an "Exclude" attribute correctly. Once I've got that done, I should be able to set up a template file.
Russ
Tuesday, June 23, 2009
ClickOnce, Team Systems, and Versioning
Unfortunately, what I discovered was that I'd have to modify the project files for all deployements when the version for Major, minor, or build changed. Since I already had 20 deployments, with the number expected to reach nearly 100, this was unacceptable. I needed a way to store version information in a single file accessible from the build.
The best solution for me is to store it in the AssemblyInfo.cs file of the ClickOnce project. This data is easily modifiable from Visual Studio in the Project's properties, plus ensured the ClickOnce manifest version matched the assembly.
Again Googling, everything was backward. Plenty of links on how to set up a dynamic AssemblyInfo.cs file, or to store version data in a modifyable file as part of the project--no, no, no! This is not what I want! When I run 20 builds, I want all 20 to have the same version number, without having to modify 20 files to do so. Generating the AssemblyInfo.cs file was completely backward and would not ensure consistent version numbers. Using a shared version file was the next best thing, but I'd have to remember to modify it manually. I couldn't have the build change it as there would be a different build number for each of the 20 deployments.
Storing it in the AssemblyInfo.cs file was best as I could easily remember to always manually modify it. It would be even better if I could somehow replace the revision with the changeset number (which the project file already gets anyhow), but I can live without it.
--I can't believe how painful it is to find the information you need--
I thought of XmlRead from MSBuild.Community.Tasks (from http://msbuildtasks.tigris.org/), but AssemblyInfo.cs is in C#, not in XML. There was also a RegexMatch task that could be a possibility, but working with Regex is always nasty. Kind of makes me feel like I'm programming in Malbolge whenever I see it.
Okay, a RegEx tester I had came up with this pattern for extracting Major, minor, and build from AssemblyInfo.cs:(\[assembly\: AssemblyVersion\(.[0-9]\.[0-9]\.[0-9])?[0-9]\.[0-9]\.[0-9]
Unfortunately, after much painful work and finally looking at the source code from msbuildtasks.tigris.org, I discovered that the RegexMatch didn't return the capture match from the Regex expression, but the entire line that the Regex expression found a match on. Useless!
So I finally gave in and created my own custom task--which apparently I should have done in the first place and saved myself the headache of trusting others.
I won't go into the code for the custom task--I'll save something like that for another post. But basically it returned all captures found on a string with a Regex expression.
I used the ReadLinesFromFile task from the Microsoft.Sdc.Tasks to import the AssemblyInfo.cs file into an ItemGroup, which I converted to a PropertyGroup with a CreateProperty task, then passed this property to my custom task.
So, I had the following added:
<ReadLinesFromFile File="$(ConfigDir)/Properties/AssemblyInfo.cs">
<Output TaskParameter="Lines"
ItemName="ItemsFromFile" />
</ReadLinesFromFile>
<!-- custom task below -->
<RegexCapture Input="@(ItemsFromFile)" Expression="(\[assembly\: AssemblyVersion\(.[0-9]\.[0-9]\.[0-9])?[0-9]\.[0-9]\.[0-9]">
<Output ItemName ="VersionData" TaskParameter="Output" />
</RegexCapture>
I used CreateProperty to convert the ItemList from the RegexCapture to a property, then referred to this property on the Regex that modified the version on the Landing Page, and on the ApplicationVersion of the MSBuild.
The revision was already in the project, using TfsVersion from MSBuild.Community Tasks from http://msbuildtasks.tigris.org/. I just concatenated this on the version information I grabbed from the AssemblyInfo file, thus ensuring each build would use an incrementing revision.
My next post: put this and my previous post together in one simplifed post, with details of what everything means.
Friday, June 19, 2009
ClickOnce and Team Systems Build
I've got it all working by overriding the AfterCompile target, but I'm not having success with the .config file modification. I can modify it fine, but it occurs at the wrong time or with the wrong file--the compile picks it up and creates the config.deploy without the modification. I found out the hard way that you can't modify a config.deploy file. If you do, you muck up the hash value that is stored in the manifest--the file's hash won't match the manifest's hash, and the application will refuse to install.
I Googled for help, and it seems everyone has advice, but none if it was very useful. Most of it was just restating
I found this one: http://coolthingoftheday.blogspot.com/2006/04/mageui-can-be-your-clickonce-stage-to.html, but it was pretty useless--just told me about Mage, but no info on how to use it. http://blog.gatosoft.com/PermaLink,guid,d0a0dd1e-c9ac-4fa9-a408-615454d49702.aspx provided a great sample, but it was designed to run MSbuild only from the command line, not from Team Systems, and the sample Deploy.proj has absolutely no comments to explain the meaning of various settings. It was useless of it to have a "GetVersion" target in there if I didn't understand why it was there. (It looks like it's getting the version fom one of the assemblies and then modifying the ClickOnce Landing page with it--is this assumption correct? or is there more? or am I just completely lost?)
I had also found http://www.codeproject.com/KB/install/QuickClickOnceArticle.aspx, but this was absolutely useless--I had already figured out all it's info just playing around with Visual Studio. That page is for newbies only.
http://blogs.msdn.com/echarran/archive/2006/08/09/693284.aspx seened to have some good clues which might have proved useful, but part of blog with the code got wacked due to the formatting of the website. Cut-and-paste did grab all of the code, at least. Then I compared it to the gatosoft.com link and saw all it was doing was giving another GenerateDeploymentManifest target override. Well, maybe it's something.
http://blogs.microsoft.co.il/blogs/maordavid/archive/2008/09/12/team-build-and-clickonce.aspx was useless--It pointed to someone else's blog that apparently is no longer on the Internet and showed just overriding the AfterCompile target. Yeah, okay--I alread knew that. Now get off of Google.
I didn't need to deal with the signing--I got that to work by cheating--just build the project on the build server using the tfsbuild user in Visual Studio. Once done one time, it never needs done again, and it's a lot easier than trying to figure out how to do it in Mage, or whatever.
Most of my problem is that I don't yet have a good handle on the meaning of the XML tags that are used in an MSBuild project. I don't want to waste my time in some class, and I can't find a nice website that just displays the cliff notes version. Microsoft's MSDN site goes into way too much detail to make sense of it quickly, and it isn't organized to give you a good summary of how to use it. All I want to know is how to get this up and running ASAP the way I need it to. I don't need to know all the excruciation of how to override labels.
I first started with the default .proj file that Team Systems creates when you create a new build. I tried taking pieces from that sample above, but none if it seems to execute at all. The default .proj file includes an AfterCompile target, which is completely missing from the above sample--so this led me down some wild goose chases trying to figure this out.
I finally got it down to the ClickOnce Landing page being modified and deployed correctly (default.htm), and all the code being correctly deployed--except the .config file still wasn't right. So, off to MSDN to find all the targets I can override--I need to override a target that occurs before the CoreCompile, but after all the sources are retrieved from source control. It also took me awhile to figure out that I needed a <SolutionToPublish> tag under an <ItemGroup> to get MSBuild to publish my solution. <SolutionToBuild> is for building only, which is useful for websites and non-clickonce applications (although I imagine <SolutionToPublish> will probably work for websites--my shop just always copies everything from the drop folder out to the website--well, my shop does it this way thanks to an overpriced consulting firm that claimed to know something and set us up that way--but that's another story).
http://msdn.microsoft.com/en-us/library/aa337604(VS.80).aspx demonstrated that AfterGet occurs exactly where I need it to, so I tried mucking with that.
I love the <Message> tag in these projects. I always add something like "~~" to the text so that I know it's my message and so that I can find it (as opposed to a message from MSBuild). These messages help me to know what's working and what's not.
First I have to deal with figuring out where the app.config file is locally. I'm using Xml.ModifyFile's XPath to modify my setting. This is pretty straight-foward and I might post info about it some other time.
First runthrough using AfterGet is simple: Message saying we're modifying App.config, then Xml.ModifyFile the app.config. Gotta do the source check-in dance and queue a new build, but fortunately the build only takes about 30 seconds. FAIL--app.config file not found. Okay, I had just specified "app.config" with no path--let me try to figure out where the Get drops all the sources--I just need to use $(SolutionRoot) as my base, then add the subfolders down to the project with the app.config. Now, remember to check out the .proj file before saving my changes, save my changes, check it back in, queue the build and wait 30 seconds--FAIL.
This time is some kind of compiler error:
CSC(0,0): error CS0006: Metadata file 'C:\Documents and Settings\tfsbuild\Local Settings\Temp\xxx.dll' could not be found
(xxx is the name of one of the referenced dlls).
Time to Google. Found http://msdn.microsoft.com/en-us/library/a92dycyz(VS.80).aspx. It says that the dll was not found. Oh, cute. This particular dll should have been compiled with this project. Even worse, the particular project that was failing was one I didn't even care if it didn't compile--it was a "sandbox" project--just something I set up to interactively test some of the custom controls. I have several possible solutions: 1. work my tail off trying to figure out how to overcome this goofy error, 2. work my tail off trying to figure out how to suppress the compile of this sandbox project, or 3. delete the project from source control. Being the lazy man I am, I opted for #3. I don't really need it anymore, anyhow. Zap! check-in, queue new build.
FAIL. Same compiler error, but this time with my critical ClickOnce project. The dll is project that is part of my solution, and this ClickOnce application references it--so why am I having a problem? I did check the app.config (based on the BuildLog.txt messages) and it is getting modified. It doesn't help tha this log file is over 1000 lines long.
Options: 1. Figure out why my ClickOnce project is not finding the DLL from another project in the same solution, or 2. Compile this project in Visual Studio and put the DLL on the ClickOnce project and reference it there. #2 is the lazy man's way of doing it, but only if the dll isn't going to change much. Unfortunately, most of the development is going on in this DLL (it's all the custom controls for the UI), so it's worth my effort to at least make a stab at option 1.
The log shows the dll getting built without error. The message "Skipping unpublishable project" is drawing my attention, but I'm moving on. Immediately after I see that it is building my ClickOnce application. The log shows "Project reference" and states the project for the dll. However, I see "Task 'MSBuild' skipped, due to false condition;" referencing the dll project, with a lot of convoluted logic explaining the false condition: @(NonVCProjectReference) !='' (evaluated to 'my dll project file' != '', a false condition) , but also it bypassed because it wasn't building inside VS. Entry is a wild-goose. moving on...I see "Done building target "ResolveProjectReferences" in project" with reference to my ClickOnce project. Promising--according to this the ClickOnce app should compile. Nothing to explain why the dll can't be found. The ugly thing is: it worked before I mucked with the app.config file. But mucking with the app.config file should have nothing to do with not being able to find the dll. Okay, let's get cute--I had removed the <SolutionToBuild> tag. Let's add it back in and see what happens. Good thing no one is paying attention to the Team Systems "Work items' thing--I've probably got hundreds by now.
Adding <SolutionToBuild> worked, and yeah! my config.deploy has the correct setting in it (intentionally different from what is in source control). Finally--for the install test. SUCCESS!!!
Okay to recap:
This is how to create a Build in Team Systems for a ClickOnce application that will modify a setting in the app.config file:
1. Log into the build machine as the tfsbuild user, open Visual Studio and publish the project. This handles the signing of the keys to the project. You only need to do this once for the project.
2. Define both the <SolutionToBuild> and the <SolutionToPublish> tags.
3. Define the "AfterGet" target override. You can use Xml.ModifyFile from Microsoft.Sdc.Tasks.dll, which comes from http://www.codeplex.com/sdctasks
4. Define the "AfterCompile" target override--this is where all the work really occurs. Best example for defining the "MSBuild" tag is http://blogs.microsoft.co.il/blogs/maordavid/archive/2008/09/12/team-build-and-clickonce.aspx, even though I stated it was rather useless. It does over-list parameters to use. I found that most of the values under the "Properties" attribute to be unnecessary, but at least it lists them, which could prove useful. I've included my working Team systems build project below, although I've obfusicated it. I left most of my variables in there to save myself work, though. Also, Note that I use the "Version" target override to adjust the ClickOnce version automatically, based on the change set. The "GetVersion" target is left in there from stuff I copied, and I don't really know if it does anything.Finally, note that I don't use MAGE or MAGEUI!!
Sample TFSBuild.Proj file:
<?xml version="1.0" encoding="utf-8"?>
<!-- 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 -->
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<!-- Do i need this?
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v8.0\TeamBuild\Microsoft.TeamFoundation.Build.targets" />
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
-->
<UsingTask AssemblyFile="Microsoft.Sdc.Tasks.dll" TaskName="Microsoft.Sdc.Tasks.Xml.ModifyFile"/>
<UsingTask AssemblyFile="Microsoft.Sdc.Tasks.dll" TaskName="Microsoft.Sdc.Tasks.File.Replace"/>
<ProjectExtensions>
<!-- Team Foundation Build Version - DO NOT CHANGE -->
<ProjectFileVersion>2</ProjectFileVersion>
<!-- DESCRIPTION
This property is included only for backwards compatibility. The description of a build definition
is now stored in the database. For compatibility with V1 clients, keep this property in sync with
the value in the database.
-->
<Description></Description>
<!-- BUILD MACHINE
This property is included only for backwards compatibility. The default machine used for a build
definition is now stored in the database, as the MachineName property of the definition's
DefaultBuildAgent. For compatibility with V1 clients, keep this property in sync with the value
in the database.
-->
<BuildMachine>UNKNOWN</BuildMachine>
</ProjectExtensions>
<PropertyGroup>
<!-- TEAM PROJECT
This property is included only for backwards compatibility. The team project for a build
definition is now stored in the database. For compatibility with V1 clients, keep this property in
sync with the value in the database.
-->
<TeamProject>put your Team Systems Project name here</TeamProject>
<!-- BUILD DIRECTORY
This property is included only for backwards compatibility. The build directory used for a build
definition is now stored in the database, as the BuildDirectory property of the definition's
DefaultBuildAgent. For compatibility with V1 clients, keep this property in sync with the value
in the database.
-->
<BuildDirectoryPath>UNKNOWN</BuildDirectoryPath>
<!-- DROP LOCATION
This property is included only for backwards compatibility. The drop location used for a build
definition is now stored in the database. For compatibility with V1 clients, keep this property
in sync with the value in the database.
-->
<DropLocation>\\UNKNOWN\drops</DropLocation>
<!-- 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="$(BuildProjectFolderPath)/path/MySolution.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="Debugx86">
<FlavorToBuild>Debug</FlavorToBuild>
<PlatformToBuild>x86</PlatformToBuild>
</ConfigurationToBuild>
The Include attribute value should be unique for each ConfigurationToBuild node.
-->
<ConfigurationToBuild Include="ReleaseAny 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>
<Major>1</Major>
<Minor>1</Minor>
<Build>5</Build>
</PropertyGroup>
<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>
-->
<Environment>-DEV</Environment>
<AppDescription>Your application description</AppDescription>
<SourceDir>E:\SystemVMS\DevBuildDrop\Release</SourceDir>
<PublishDir>$(SourceDir)\Publish</PublishDir>
<SupportUrl>http://whatever.com</SupportUrl>
<!-- signing certificate below -->
<SigningCert>ClickOnce_TemporaryKey.pfx</SigningCert>
<Company>My Company, Inc.</Company>
<SettingsFile>E:\SystemVMS\DevBuildDrop\settings.xml</SettingsFile>
<SolutionName>MySolution</SolutionName>
<AppConfigFile>app.config</AppConfigFile>
<CustomSetting>My setting</CustomSetting>
</PropertyGroup>
<PropertyGroup>
<!--<ConfigDir>$(SolutionRoot)\..\Binaries\Release\</ConfigDir>-->
<ConfigDir>$(SolutionRoot)\$(SolutionName)$(Environment)\$(SolutionName)\</ConfigDir>
<LocalClickOnceVirtualRootDir>ClickOncePublishing</LocalClickOnceVirtualRootDir>
<RemoteClickOnceVirtualRootDir>\\whatever\e$\Services\$(SolutionName)2\</RemoteClickOnceVirtualRootDir>
<ClickOnceUrl>http://whatever.com/$(SolutionName)/default.htm</ClickOnceUrl>
<ClickOnceApplicationUrl>$(ClickOnceUrl)$(SolutionName).application</ClickOnceApplicationUrl>
<ImageDirectory>$(SolutionRoot)\$(SolutionName)$(Environment)\Referenced Binaries</ImageDirectory>
<ClickOnceAppTitle>My ClickOnce Application</ClickOnceAppTitle>
<CurrentProdBuildFile>E:\SystemVMS\DevBuildDrop\settings.xml</CurrentProdBuildFile>
</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>
<ItemGroup>
<ClickOnceInstallationFiles Include="$(SolutionName).application"/>
<ClickOnceInstallationFiles Include="$(SolutionName).exe.manifest"/>
<ClickOnceInstallationFiles Include="setup.exe"/>
<ClickOnceInstallationFiles Include="default.htm"/>
<ClickOnceLandingPage Include="$(SolutionRoot)\$(SolutionName)$(Environment)\Referenced Binaries\ClickOnceLandingPage.htm"/>
</ItemGroup>
<Target Name="Version">
<TfsVersion LocalPath="$(SolutionRoot)">
<Output TaskParameter="Changeset" PropertyName="Revision"/>
</TfsVersion>
</Target>
<Target Name="GetVersion">
<Message Text=" ~~~ Getting version info..."/>
<GetAssemblyIdentity AssemblyFiles="@(GetVersionAssembly)">
<Output TaskParameter="Assemblies"
ItemName="GetVersionAssemblyInfo"/>
</GetAssemblyIdentity>
</Target>
<Target Name="AfterGet">
<Message Text=" ~~~ Doing After Get"/>
<!-- Edit Config File here.-->
<Message Text=" ~~~ Modifying Config file for MyUserSetting: AppConfigFile=$(ConfigDir)$(AppConfigFile)"/>
<ModifyFile
Path="$(ConfigDir)$(AppConfigFile)"
XPath="/configuration/userSettings/$(SolutionName).Properties.Settings/setting[@name='MyUserSetting']/value"
NewValue="$(CustomSetting)"
Force="true" />
</Target>
<Target Name="AfterCompile" DependsOnTargets="Version">
<Message Text=" ~~~ BuildProjectFolderPath=$(BuildProjectFolderPath)"/>
<Message Text=" ~~~ SolutionRoot=$(SolutionRoot)"/>
<Message Text=" ~~~ copying @(ClickOnceLandingPage) to $(RemoteClickOnceVirtualRootDir)\default.htm"/>
<Copy SourceFiles="@(ClickOnceLandingPage)"
DestinationFiles="$(RemoteClickOnceVirtualRootDir)\default.htm"/>
<Message Text=" ~~~ Modifying landing page for app name, title, and version"/>
<!-- TODO: change to use File.RegEx -->
<ModifyFile
Path="$(RemoteClickOnceVirtualRootDir)\default.htm"
RegularExpression="#APPLICATION_NAME#"
NewValue="$(SolutionName)"
Force="true"/>
<ModifyFile
Path="$(RemoteClickOnceVirtualRootDir)\default.htm"
RegularExpression="#TITLE#"
NewValue="$(ClickOnceAppTitle)"
Force="true"/>
<ModifyFile
Path="$(RemoteClickOnceVirtualRootDir)\default.htm"
RegularExpression="#VERSION#"
NewValue="$(Major).$(Minor).$(Build).$(Revision)"
Force="true"/>
<Message Text=" ~~~ Building"/>
<MSBuild
Projects="$(SolutionRoot)/$(SolutionName)$(Environment)/$(SolutionName)/$(SolutionName).csproj"
Properties="Configuration=%(ConfigurationToBuild.FlavorToBuild);PublishDir=$(RemoteClickOnceVirtualRootDir);OutDir=$(RemoteClickOnceVirtualRootDir);ApplicationVersion=$(Major).$(Minor).$(Build).$(Revision);"
Targets="Publish" />
</Target>
</Project>