.Net ramblings
# Tuesday, 24 April 2012
Using XStandard dll on a Win 64-bit

I have a winforms app that ran into trouble with the XStandard XHTML component on a 64 bit system. I was registering the OCX file from code if it didn't register via the MSI. found this kb article useful, and then called the appropriate version of regsvr32.exe based on the platform.

try
{
    // attempt to load the control 
    this.pageEditor = new PageEditor();
}
catch (System.Runtime.InteropServices.COMException)
{
    // control loading failed
    string ocxPath = Path.Combine(Application.StartupPath, "Xstandard.ocx");
    try
    {
        // OCX is not registered, register it now using the Syswow64 version of regsvr32.exe if we are using a 64 bit system
        Process p = new Process();
        p.StartInfo.FileName = @"regsvr32.exe";
        p.StartInfo.WorkingDirectory = (IntPtr.Size == 8) ? @"%SystemRoot%\Syswow64" : @"%SystemRoot%\system32";  // IntPtr is size 8 on a 64 bit system
        p.StartInfo.UseShellExecute = false;
        p.StartInfo.Arguments = String.Format("/s \"{0}\"", ocxPath);
        p.Start();
        p.Close();
        MessageBox.Show("Please restart... (OCX file registered)", "Restart");
        Application.Exit();
        return;
    }
    catch (Exception ex)
    {
        MessageBox.Show("Unable to register OCX file: " + ex.Message, "Error");
        Application.Exit();
        return;
    }
}

Tuesday, 24 April 2012 20:51:07 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net Windows Forms

# Monday, 16 January 2012
.Net windows forms Multi-line ComboBox / text-wrapping
seems like the most basic requirement for a list control... but there we are.  use this as a replacement for the System.Windows.Forms.ComboBox.

using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Collections.Generic;

namespace HortLaptopApp
{
    class ComboBoxWrap : ComboBox
    {
        // ref http://stackoverflow.com/questions/1245530/unable-to-set-the-dropdownheight-of-combobox
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);

        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);

        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int Left;        // x position of upper-left corner
            public int Top;         // y position of upper-left corner
            public int Right;       // x position of lower-right corner
            public int Bottom;      // y position of lower-right corner
        }

        public const int SWP_NOZORDER = 0x0004;
        public const int SWP_NOACTIVATE = 0x0010;
        public const int SWP_FRAMECHANGED = 0x0020;
        public const int SWP_NOOWNERZORDER = 0x0200;

        public const int WM_CTLCOLORLISTBOX = 0x0134;

        private int _hwndDropDown = 0;

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == WM_CTLCOLORLISTBOX)
            {
                if (_hwndDropDown == 0)
                {
                    _hwndDropDown = m.LParam.ToInt32();

                    RECT r;
                    GetWindowRect((IntPtr)_hwndDropDown, out r);

                    //int newHeight = 0;
                   // for(int i=0; i<Items.Count && i < MaxDropDownItems; i++)
                    //    newHeight += this.GetItemHeight(i);

                    int total = 0;
                    for (int i = 0; i < this.Items.Count; i++)
                        total += this.GetItemHeight(i);
                    this.DropDownHeight = total + SystemInformation.BorderSize.Height * (this.Items.Count + 2);
           

                    SetWindowPos((IntPtr)_hwndDropDown, IntPtr.Zero,
                        r.Left,
                                 r.Top,
                                 DropDownWidth,
                                 DropDownHeight,
                                 SWP_FRAMECHANGED |
                                     SWP_NOACTIVATE |
                                     SWP_NOZORDER |
                                     SWP_NOOWNERZORDER);
                }
            }

            base.WndProc(ref m);
        }

        protected override void OnDropDownClosed(EventArgs e)
        {
            _hwndDropDown = 0;
            base.OnDropDownClosed(e);
        }

        public ComboBoxWrap() : base()
        {
            // add event handlers
            this.DrawMode = System.Windows.Forms.DrawMode.OwnerDrawVariable;
            this.DrawItem += new DrawItemEventHandler(ComboBoxWrap_DrawItem);
            this.MeasureItem += new MeasureItemEventHandler(ComboBoxWrap_MeasureItem);
        }

        void ComboBoxWrap_MeasureItem(object sender, MeasureItemEventArgs e)
        {
            // set the height of the item, using MeasureString with the font and control width
            ComboBoxWrap ddl = (ComboBoxWrap)sender;
            string text = ddl.Items[e.Index].ToString();
            SizeF size = e.Graphics.MeasureString(text, this.Font, ddl.DropDownWidth); 
            e.ItemHeight = (int)Math.Ceiling(size.Height) + 1;  // plus one for the border
            e.ItemWidth = ddl.DropDownWidth;
            System.Diagnostics.Trace.WriteLine(String.Format("Height {0}, Text {1}", e.ItemHeight, text));
        }

        void ComboBoxWrap_DrawItem(object sender, DrawItemEventArgs e)
        {
            if (e.Index < 0)
                return;

            // draw a lighter blue selected BG colour, the dark blue default has poor contrast with black text on a dark blue background
            if ((e.State & DrawItemState.Selected) == DrawItemState.Selected)
                e.Graphics.FillRectangle(Brushes.PowderBlue, e.Bounds);
            else
                e.Graphics.FillRectangle(Brushes.White, e.Bounds);
            
            // get the text of the item
            ComboBoxWrap ddl = (ComboBoxWrap)sender;
            string text = ddl.Items[e.Index].ToString();

            // don't dispose the brush afterwards
            Brush b = Brushes.Black;
            e.Graphics.DrawString(text, this.Font, b, e.Bounds, StringFormat.GenericDefault);
            
            // draw a light grey border line to separate the items
            Pen p = new Pen(Brushes.Gainsboro, 1);
            e.Graphics.DrawLine(p, new Point(e.Bounds.Left, e.Bounds.Bottom-1), new Point(e.Bounds.Right, e.Bounds.Bottom-1));
            p.Dispose();
            
            e.DrawFocusRectangle();
        }
    }
}


Monday, 16 January 2012 10:08:16 (GMT Standard Time, UTC+00:00)  #    Comments [1]  .Net Windows Forms

# Friday, 16 November 2007
Running WSE 2 and 3 side by side

Note: this doesn't really work..

i'm just leaving it here for interest.  the approach will work for the first invocation of either v2 or v3 client, but after a v2 client has used the web service, any v3 clients will fail.  MS told me this is not supported because of the 2 soapExtensions interfere with each other in the pipeline etc


My asp.net CMS has some winforms clients running WSE2, and i'm doing an upgrade to the client (and server) to use WSE3, but i want to support both versions.  Both WSE2 and WSE3 clients are using a CustomUsernameTokenManager.  it was a bit tricky to work out, but here is what i ended up doing. thanks to brian o'keefe for his newsgroup post for most of the answer.  Your exact config might vary but hopefully it will save you some of the hassle of working all this out.
  • Leave the existing web service in place, e.g. Service_WSE2.asmx
  • Create a new web service for the WSE3 client, essentially copy/paste the ASMX file, e.g. Service_WSE3.asmx
  • Create a new CustomUsernameTokenManager for the WSE3 service which inherits from Microsoft.Web.Services3.Security.Tokens.UsernameTokenManager.  In my case it was as easy as copy/paste from the WSE2 token manager and change all the WSE2 namespaces to WSE3.
  • then in web.config, make sure both WSE config sections are listed:
    <configuration>
    <configSections>
    <section name="microsoft.web.services3" type="Microsoft.Web.Services3.Configuration.WebServicesConfiguration, Microsoft.Web.Services3,Version=3.0.0.0,Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <section name="microsoft.web.services2" type="Microsoft.Web.Services2.Configuration.WebServicesConfiguration, Microsoft.Web.Services2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </configSections>
  • add in references to both the assemblies for the compilation section (not 100% sure if this is necessary, but at least it means the app will not compile on the production server if you forget to deploy either of the WSE assemblies, instead of waiting till one of your clients connects).

    	<compilation defaultLanguage="c#" debug="true">
    <assemblies>
    <add assembly="Microsoft.Web.Services2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
    <add assembly="Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
  • remove the <webServices> section from <system.web>, because each web service should be configured by location, not global as this interferes.

    	<location path="Service_WSE2.asmx">
    <system.web>
    <webServices>
    <soapExtensionTypes>
    <add type="Microsoft.Web.Services2.WebServicesExtension, Microsoft.Web.Services2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" priority="1" group="0" />
    </soapExtensionTypes>
    </webServices>
    </system.web>
    </location>

    <location path="Service_WSE3.asmx">
    <system.web>
    <webServices>
    <soapExtensionTypes>
    <clear/>
    </soapExtensionTypes>
    <soapServerProtocolFactory type="Microsoft.Web.Services3.WseProtocolFactory, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
    <soapExtensionImporterTypes>
    <add type="Microsoft.Web.Services3.Description.WseExtensionImporter, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
    </soapExtensionImporterTypes>
    </webServices>
    </system.web>
    </location>
  • my webservice policy are defined in policyCache.config files, so the following web.config sections point each version of WSE to the right file:
    	<microsoft.web.services3>
    <policy fileName="policyCache_WSE3.config"/>
    <security>
    <securityTokenManager>
    <add type="Whatever.CustomUsernameTokenManager3" namespace="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" localName="UsernameToken"/>
    </securityTokenManager>
    </security>
    </microsoft.web.services3>
    <microsoft.web.services2>
    <messaging>
    <maxRequestLength>10000</maxRequestLength>
    </messaging>
    <security>
    <securityTokenManager type="Whatever.CustomUsernameTokenManager2, MyAssemblyName" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
    qname="wsse:UsernameToken" />
    <defaultTtlInSeconds>86400</defaultTtlInSeconds>
    <timeToleranceInSeconds>86400</timeToleranceInSeconds>
    </security>
    <policy>
    <cache name="policyCache_WSE2.config" />
    </policy>
    </microsoft.web.services2>
  • and lastly, the policy cache files themselves:
    WSE 3 version
    <?xml version="1.0"?>
    <policies xmlns="http://schemas.microsoft.com/wse/2005/06/policy">
    <extensions>
    <extension name="usernameOverTransportSecurity" type="Microsoft.Web.Services3.Design.UsernameOverTransportAssertion, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    <extension name="requireActionHeader" type="Microsoft.Web.Services3.Design.RequireActionHeaderAssertion, Microsoft.Web.Services3, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
    </extensions>
    <policy name="usernameTokenSecurity">
    <usernameOverTransportSecurity />
    <requireActionHeader />
    </policy>
    </policies>
  • WSE 2 version
    <?xml version="1.0"?>
    <policyDocument xmlns:wse="http://schemas.microsoft.com/wse/2003/06/Policy" xmlns="http://schemas.microsoft.com/wse/2003/06/Policy">
    <mappings xmlns:wse="http://schemas.microsoft.com/wse/2003/06/Policy">

    <endpoint uri="http://localhost/Service_WSE2.asmx">
    <defaultOperation>
    <request policy="#username-token-signed" />
    <response policy="" />
    <fault policy="" />
    </defaultOperation>
    </endpoint>

    <defaultEndpoint>
    <defaultOperation>
    <request policy="" />
    <response policy="" />
    <fault policy="" />
    </defaultOperation>
    </defaultEndpoint>

    </mappings>
    <policies xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <wsp:Policy wsu:Id="username-token-signed" xmlns:wsp="http://schemas.xmlsoap.org/ws/2002/12/policy" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing" xmlns:wssp="http://schemas.xmlsoap.org/ws/2002/12/secext">
    <wsp:MessagePredicate wsp:Usage="wsp:Required" Dialect="http://schemas.xmlsoap.org/2002/12/wsse#part">
    wsp:Body() wsp:Header(wsa:To) wsp:Header(wsa:Action) wsp:Header(wsa:MessageID) wse:Timestamp()
    </wsp:MessagePredicate>
    <wssp:Integrity wsp:Usage="wsp:Required">
    <wssp:TokenInfo>
    <SecurityToken xmlns="http://schemas.xmlsoap.org/ws/2002/12/secext">
    <wssp:TokenType>http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#UsernameToken</wssp:TokenType>
    <wssp:Claims>
    <wssp:UsePassword wsp:Usage="wsp:Required" />
    </wssp:Claims>
    </SecurityToken>
    </wssp:TokenInfo>
    <wssp:MessageParts Dialect="http://schemas.xmlsoap.org/2002/12/wsse#part">
    wsp:Body() wsp:Header(wsa:Action) wsp:Header(wsa:FaultTo) wsp:Header(wsa:From) wsp:Header(wsa:MessageID) wsp:Header(wsa:RelatesTo) wsp:Header(wsa:ReplyTo) wsp:Header(wsa:To) wse:Timestamp()
    </wssp:MessageParts>
    </wssp:Integrity>
    </wsp:Policy>
    </policies>
    </policyDocument>


Friday, 16 November 2007 12:16:50 (GMT Standard Time, UTC+00:00)  #    Comments [1]  .Net General | .Net Windows Forms | Asp.Net

# Tuesday, 02 October 2007
Bug in ComboBox SelectedIndex
big bug in ComboBox control here: http://support.microsoft.com/default.aspx?scid=kb;en-us;327244
you have to set the SelectedIndex to -1 twice in a row if you want it to take effect!
i came across this in a compact framework app, but it looks like it applies to win-forms as well.


Tuesday, 02 October 2007 17:17:18 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net Compact | .Net Windows Forms

# Friday, 27 July 2007
VS 2008: First impressions
Just got up and running with VS 2008 Beta 2 and converted my Orcas Beta 1 projects over to VS 2008.  have a read of Scott Gu's post on downloading and installing.  whaddya know? VS 2008 hasn't crashed yet :)  seriously though, this is a major relief to see good stability.  as a developer it did my head in to have to keep nuking devenv.exe and losing a few minutes work, driving me quietly insane.  hopefully those days are over now.  Beta 1 was a thousand times better than VS 2005, but Beta 2 looks even better. 

in terms of breaking changes with upgrading projects etc., here are some tidbits i came across
  • the namespace System.Data.Linq.Expressions no longer exists.  i just deleted this namespace and it worked fine, they must have moved various classes back into the root Linq namespace or something.
  • if you use SqlMetal, you will have to use the new version of this tool, which i found at C:\Program Files\Microsoft SDKs\Windows\V6.0A\Bin\SqlMetal.exe. The generated code from the new tool is very different to before so you will need to regenerate in order to use the new LINQ libraries.
  • web.config changes are also introduced.  the best way to figure out the changes is to open up a blank web application and compare the standard web.config in the empty project to your own web.config.
  • Crystal Reports assembly version numbers are the same with this release of VS, 10.5.3700.0, so no changes here since Beta1.
i'll post more next week when i have some real experience using VS 2008.  so far so good.  great job Scott & Co.

Update 30 July 2007 - Deployment Issues
  • To deploy a VS 2008 beta 2 web application to a Server 2003 with .net runtime 2.0, you need to install 3.5 of the runtime.  i got all sorts of web.config errors due to the new syntax with beta 2.  A web app compiled against v3.5 in Beta 1 had no trouble running on top of the 2.0 runtime with the various v3.5 DLLs deployed to the /bin directory, however this doesn't seem to be possible anymore.  it took me a while to find the correct download for the 3.5 beta 2 redistributable, it installed in about 10 mins and required a reboot, and did not affect any of the v1.1 and v2 web sites running on the server. 


Friday, 27 July 2007 17:49:16 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net General | .Net Windows Forms | Asp.Net

# Wednesday, 18 July 2007
JPEG Resizing Quality Problems .Net
public static ImageCodecInfo GetEncoderInfo(String mimeType)
{
int j;
ImageCodecInfo[] encoders;
encoders = ImageCodecInfo.GetImageEncoders();
for(j = 0; j < encoders.Length; ++j)
if(encoders[j].MimeType == mimeType)
return encoders[j];
return null;
}

public static Bitmap resizeImage(Bitmap originalImage, int max)
{
// never increase the size of an image from this method because it loses resolution
if(originalImage.Height <= max && originalImage.Width <= max)
return originalImage;

int height, width;
if(originalImage.Width > originalImage.Height)
{
width = max;
height = (int)(max/((double)originalImage.Width / (double)originalImage.Height));
}
else
{
height = max;
width = (int)(max/((double)originalImage.Height / (double)originalImage.Width));
}

Bitmap thumb = new Bitmap(originalImage as Image, width, height);
Graphics resizer = Graphics.FromImage(thumb);
resizer.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
resizer.DrawImage(originalImage, 0, 0, width, height);

resizer.Dispose();
return thumb;
}

public static bool ThumbnailCallback()
{
return false;
}

this lovely code highlighted in red solved problems i was having with resizing JPEGs using .net.   without the code in red, some images would appear very grainy when reduced in size.  i got this tip from http://forums.asp.net/p/532033/532033.aspx.



Wednesday, 18 July 2007 17:41:41 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net General | .Net Windows Forms

# Friday, 11 August 2006
Crystal Reports: Export PDF irregularities between server and dev machine
I have a few reports in my web application that i export in PDF format only.  You would think this would remove any printer driver complications but apparently not.
Crystal Reports have a surprisingly good article to help troubleshoot issues with printing or exporting a report.

The problem i was finding was that in a Text object or Unbound field, the text would be truncated at the edge of the box boundary.  Some users have reported strange mid-word wrapping, but mine was a different problem, actually hacking off the end of each line.  This didn't happen on my dev machine, but it did happen on the server (2003).  When i compared them side by side, the Times New Roman font on the server was stretched by about 5% compared to my dev machine.
with the help of the CR article, i tracked it down to the lack of any printer installed on the server, i had also disabled the Print Spooler service which was preventing even the Microsoft Office Document Imaging Printer from being available.  So i started the print spooler service and made sure that the Office Imaging Printer was marked as the default printer.  Then i went back to Visual Studio and set all the reports to use the Office Imaging printer.  This solved the problem. 

You could of course install a PDF only printer such as PrimoPDF if you didn't have MS office installed on the server. As long as the server has the same printer installed as the one set in the report design, everything turns out fine.


Friday, 11 August 2006 13:20:57 (GMT Daylight Time, UTC+01:00)  #    Comments [1]  .Net General | .Net Windows Forms | Asp.Net

# Thursday, 10 August 2006
Crystal Reports: join / concatenate null strings
I was getting blank fields in my crystal report if one of the values in the formula was null.
Thanks to this post i know better than to trust Crystal's crummy concatenation rules.  Here is what Gundula Wangerin had to say:
A string concatenated with null always gives null.
You have to check if the first name is null with
     if not IsNull(firstname) then 
        lastname + " " + firstname
    else
        lastname


Thursday, 10 August 2006 17:31:20 (GMT Daylight Time, UTC+01:00)  #    Comments [3]  .Net General | .Net Windows Forms | Asp.Net

# Friday, 17 February 2006
FtpWebRequest with ProgressBar [C# Ftp]
Screenshot of upload operationhi,
i was delighted to see .Net 2.0 has an excellent class for working with FTP.  looking back i can't believe it wasn't part of 1.1. 

i noticed there was no built-in support for reporting the progress of an FTP operation to a user interface.  I needed this functionality in an app i'm working on so i built a class and i'm posting it here in case anyone is interested.

The class FtpProgress inherits from BackgroundWorker, so you just drag an FtpProgress component onto your form, hook up events for Completed and ProgressChanged, and your laughing.  There is a mini class called FtpSettings which stores all the connect info etc to pass in as a paramter to the RunWorkerAsync() method.

I have a variable called 'ChunkSize' which determines the size of each buffer written to the FtpWebRequest output stream.  It is set to 4k by default, and you can increase this if you want for very fast networks but i don't think it will make much difference.  I think the overhead is negligible.

Download the source code and demo application [30 k].

It only does upload, so if you want download functionality, you can do the mirror-image of the upload code, i.e. instead of writing buffers to the output stream, read it from the input stream, or however that would work :) 

here is all the code you need to start an Upload, and report progress:
private void btnUpload_Click(object sender, EventArgs e)
{
// create a new FtpSettings class to store all the paramaters for the FtpProgress thread
FtpSettings f = new FtpSettings();
f.Host = this.txtHost.Text;
f.Username = this.txtUsername.Text;
f.Password = this.txtPassword.Text;
f.TargetFolder = this.txtDir.Text;
f.SourceFile = this.txtUploadFile.Text;
f.Passive = this.chkPassive.Checked;
f.Port = Int32.Parse(this.txtPort.Text);
this.ftpProgress1.RunWorkerAsync(f);
}

private void ftpProgress1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.toolStripStatusLabel1.Text = e.UserState.ToString(); // the message will be something like: 45 Kb / 102.12 Mb
this.toolStripProgressBar1.Value = Math.Min(this.toolStripProgressBar1.Maximum, e.ProgressPercentage);
}

Friday, 17 February 2006 18:09:05 (GMT Standard Time, UTC+00:00)  #    Comments [29]  .Net General | .Net Windows Forms

# Thursday, 02 February 2006
Using Xml DOM with MetaBase.xml
For some reason i can't get SelectNodes to work with the IIS 6 MetaBase.xml file.  It just doesn't return any matches.  I think the structure may be non-standard or too complex, or else i'm not using it right.  but in any case, I worked around the problem by opening the file with File.OpenText and using Regex to parse out the nodes i'm interested in, namely the IISWebServer nodes.

string MetaBase = "";
using(StreamReader sr = File.OpenText(ConfigurationManager.AppSettings["MetaBasePath"].ToString()))
MetaBase = sr.ReadToEnd();

foreach(Match m in new Regex(@"(?x:)<IIsWebServer[^>]*>.*?</IIsWebServer>", RegexOptions.IgnoreCase).Matches(MetaBase))
{
XmlDocument doc = new XmlDocument();
string IisCode = "", ServerBindings = "", ServerComment = "";
doc.LoadXml(m.Groups[0].Value);
XmlElement node = doc.DocumentElement;
// now you can access the node attributes
...


Thursday, 02 February 2006 13:38:34 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | .Net Windows Forms | Asp.Net | Windows Server

# Wednesday, 25 January 2006
Crystal Reports TextObject ignores newline, carriage return, \r\n
I have a text object in my crystal report, and i set the value programatically for it, using something like this:
(rpt.Section3.ReportObjects["txtDate"] as TextObject).Text = DateTime.Now.ToShortDateString();
this works fine, until you put a string with line breaks inside it, specifically \r\n or the carriage return character.  This is a bug in CR for .Net, you can read the official blurb here.  the work-around code they post is in VB, and it made no sense to me when i read it.
what they actually do is make you change the TextObject into a FormulaField (you have to view the 'Field Explorer' tab next to the toolbox, and drag on a Formula Field).  Then you set 'CanGrow' to true, and then you go back to your code, and do the most arcane work-around i've ever seen.  you set the formula to your text string, but you must surround it in single quote characters, and replace \r\n with some inline managed code as follows: ' + chr(10) + '
when i first read this, i thought they had made a syntax error, but this is the way to do it, and it works.  the new format for setting the formula field is:
rpt.DataDefinition.FormulaFields[0].Text = "'" + YourString.Replace("\r\n", "' + chr(10) + '") + "'";


Wednesday, 25 January 2006 13:52:05 (GMT Standard Time, UTC+00:00)  #    Comments [16]  .Net General | .Net Windows Forms | Asp.Net

# Monday, 09 January 2006
Xml Serialization: Properties left out without 'set' accessor
I ran into a frustrating problem today where some properties of my objects weren't getting serialized.  I noticed it was the ones without 'set' accessors.  I didn't want to put set accessors in because the properties should be read-only, but apparently the xml serializer needs the set accessor to de-serialize. 
there is a good discussion on it on this thread.
Luckily we can use the ReadOnlyAttribute setting on the property to prevent it being modified by a programmer at design time.
or you can use the Soap serializer instead of xml, whatever that is.


Monday, 09 January 2006 17:57:34 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | .Net Windows Forms | Asp.Net

# Thursday, 29 December 2005
Sending files in chunks with MTOM Web Services and .NET 2.0
just posting my code-project article on http chunking web services with MTOM here for reference.  by the way it has been updated on codeproject...

Screenshot of windows forms client, uploading a file

Introduction

In trying to keep up to speed with .NET 2.0, I decided to do a .NET 2.0 version of my CodeProject article "DIME Buffered Upload" which used the DIME standard to transfer binary data over web services. The DIME approach was reasonably efficient but the code is quite complex and I was keen to explore what .NET 2.0 has to offer. In this article, I use version 3.0 of the WSE (Web Service Enhancements) which is available for .NET 2.0 as an add-in, to provide a simpler and faster method of sending binary data in small chunks over HTTP web services.

Background

Just a recap on why you may need to send data in small chunks at all: if you have a large file and you want to send it across a web service, you must understand the way it all fits together between IIS, .NET, and the web service call. You send your file as an array of bytes, as a parameter to a web service call, which is all sent to the IIS web server as a single request. This is bad if the size of the file is beyond the configured MaxRequestLength of your application, or if the request causes an IIS timeout. It is also bad from the point of view of providing feedback of the file transfer to the user interface because you have no indication how the transfer is going, until it is either completed or failed. The solution outlined here is to send chunks of the file one by one, and append them to the file on the server.

There is an MD5 file hash done on the client and the server to verify that the file received is identical to the file sent.

Also, there is an upload and download code included in this article.

Adventures with MTOM

MTOM stands for SOAP "Message Transmission Optimization Mechanism" and it is a W3C standard. To use it (and to run this application), you must download and install WSE 3.0, which includes MTOM support for the first time. If you look in the app.config and web.config files in the source code, you will see sections referring to the WSE 3 assembly, and a messaging clientMode or serverMode setting. These are necessary to run MTOM in the application.

The problem with DIME was that the binary content of the message was sent outside the SoapEnvelope of the XML message. This meant that although your message was secure, the Dime Attachment may not be secure. MTOM fully complies with the other WS-* specifications (like WS-Security) so the entire message is secure.

It took me a while to realise that when MTOM is turned on for the client and the server, WSE automatically handles the binary encoding of the data in the web service message. With DIME and WSE 2.0, you had to configure your app for DIME and then use DimeAttachments in your code. This is no longer necessary, you just send your byte[] as a parameter or return value, and WSE makes sure that it is sent as binary, and not padded by XML serialization as it would be in the absence of DIME or MTOM.

How it works

The web service has two main methods, AppendChunk is for uploading a file to the server, DownloadChunk is for downloading from the server. These methods receive parameters for the file name, the offset of the chunk, and the size of the buffer being sent/received.

The Windows Forms client application can upload a file by sending all the chunks one after the other using AppendChunk, until the file has been completely sent. It can do an MD5 hash on the local file, and compare it with the hash on the file on the server, to make sure the contents of the files are identical. The download code is very similar, the main difference is that the client must know from the server how big the file is, so that it can know when to stop requesting chunks.

A simplified version of the upload code is shown below (from the WinForms client). Have a look in the code for Form1.cs to see the inline comments + the explanation of the code. Essentially, a file stream is opened on the client for the duration of the transfer. Then the first chunk is read into the Buffer byte array. The while loop keeps running until the FileStream.Read() method returns 0, i.e. the end of the file has been reached. For each iteration, the buffer is sent directly to the web service as a byte[]. The 'SentBytes' variable is used to report progress to the form.

using(FileStream fs = new FileStream(LocalFilePath, FileMode.Open, FileAccess.Read))
{
int BytesRead = fs.Read(Buffer, 0, ChunkSize);
while(BytesRead > 0 && !worker.CancellationPending)
{
ws.AppendChunk(FileName, Buffer, SentBytes, BytesRead);
SentBytes += BytesRead;
BytesRead = fs.Read(Buffer, 0, ChunkSize);
}
}

Example of the BackgroundWorker class in .NET 2.0

.NET 2.0 has a great new class called 'BackgroundWorker' to simplify running tasks asynchronously. Although this application sends the file in small chunks, even these small chunks would delay the WinForms application and make it look crashed during the transfer. So the web service calls still need to be done asynchronously. The BackgroundWorker class works using an event model, where you have code sections to run for DoWork (when you start), ProgressChanged (to update your progress bar / status bar), and Completed (or failed). You can pass parameters to the DoWork method, which you could not do with the Thread class in .NET 1.1 (I know you could with delegates, but delegates aren't great for thread control). You can also access the return value of DoWork in the Completed event handler. So for once, MS has thought of everything and made a very clean threading model. Exceptions are handled internally and you can access them in the Completed method via the RunWorkerCompletedEventArgs.Error property.

The code shown below is an example of the ProgressChanged event handler:

private void workerUpload_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// update the progress bar and status bar text
this.toolStripProgressBar1.Value = e.ProgressPercentage;
this.statusText.Text = e.UserState.ToString();
// summary text is sent in the UserState parameter
}

I have used four BackgroundWorker objects in the application:

  • one to manage the upload process,
  • one to manage the download process,
  • another to calculate the local MD5 file hash in parallel while waiting for the server result,
  • and another to download the list of files in the Upload server folder to allow the user to select a file to download.

The reason I use a BackgroundWorker object for each task is because the code for each task is tied in to the events for that object.

A good example of Thread.Join()

When the upload or download is complete, the client asks for an MD5 hash of the file on the server, so it can compare it with the local file to make sure they are identical. I originally did these in sequence. But it can take a few seconds to calculate the result for a large file (anything over a few hundred MB), so the application was waiting five seconds for the server to calculate the hash, and then five more seconds for the client to calculate its own hash. This made no sense, so I decided to implement a multi-threaded approach to allow them to run in parallel. While the client is waiting on the server, it should be calculating its own file hash. This is done with the Thread class, and the use of the Join() method which blocks execution until the thread is complete.

The code below shows how this is accomplished:

// start calculating the local hash (stored in class variable)
this.hashThread = new Thread(new ThreadStart(this.CheckFileHash));
this.hashThread.Start();

// request the server hash
string ServerFileHash = ws.CheckFileHash(FileName);

// wait for the local hash to complete
this.hashThread.Join();

if(this.LocalFileHash == ServerFileHash)
e.Result = "Hashes match exactly";
else
e.Result = "Hashes do not match";

There is a good chance that the two operations will finish at approximately the same time, so very little waiting around will actually happen.

Performance compared with DIME

I found that MTOM was about 10% faster than DIME in my limited testing. This is probably to do with the need to package up each chunk into a DIME attachment, which is no longer necessary with MTOM. I was able to upload files of several gigabytes in size without problems.

Obviously, there is an overhead with all this business of reading file chunks and appending them, so the larger the chunk size, the more efficient your application will be. It should be customised based on the network and the expected size of files. For very small files, it is no harm to use small chunk sizes (e.g., 32 Kb) because this will give accurate and regular feedback to the user interface. For very large files on a fast network, consider using 4000 Kb to make good use of the bandwidth and reduce the File Input/Output overhead. If you want to send chunks larger than 4 MB, you must increase the .NET 2.0 Max Request Size limit in your web.config.

Conclusions

Feel free to use this code and modify it as you please. Please post a comment for any bugs, suggestions, or improvements. Enjoy!


Thursday, 29 December 2005 20:34:15 (GMT Standard Time, UTC+00:00)  #    Comments [24]  .Net General | .Net Windows Forms | Asp.Net

# Friday, 02 December 2005
ADO.NET timeouts when filling a DataAdapter in the middle of a transaction

I had a transaction comprised of about 5 commands, and in the middle of it, i needed to do a SELECT on a table to know what values to insert for one of the commands.  I encountered a timeout just after i called Fill on the DataAdapter.  it took me a while to figure out that it was because the transaction had already taken out a lock on the table i was selecting from.  makes sense now, but i spent ages on it!  just posting it here in case anyone else runs into the same problem.


Friday, 02 December 2005 12:26:22 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net General | .Net Windows Forms | Asp.Net | Database

# Saturday, 27 August 2005
A 'Progress-Task-List' control

I decided to write this control when i realised there are some cases where a progress bar is not enough, even with a label that gets updated for each new stage of a list of operations.

Sometimes you want the user to see what's coming next, what has already been done, and the ProgressTaskList control does just that.  it's nothing new of course, often used in installers and the like, but there didn't appear to be any .Net control like this.

I've submitted the control to code-project (URL to follow), which is where any updates will be posted.  If you have comments or suggestions, it's best to put them on the code-project site please.

I'm quite pleased with the way it turned out.  The code is very simple, and i couldn't find any bugs having done lots of testing.

As you can see in the screenshot to the left, it handles scrolling quite well, and automatically jumps to the current task when it is starting a new one.

To use it you just specify TaskItems as a string[] and call Start() to set it off. Then call NextTask() every time a task is finished to advance it to the next task. 

You can download the source here (30Kb) if you like, but check on code-project for updates.


Saturday, 27 August 2005 01:28:29 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net Windows Forms

# Monday, 22 November 2004
How to disable new rows in Windows Forms DataGrid

i found a few work arounds to prevent a datagrid from displaying the * new row. one of them involved using a dataview as the datasource, with AllowNew property set to false. however, someone called Sameers from theAngrycodeR pointed out that a datatable has a DefaultView property which also has this AllowNew property. so you can use the following code (if your datasource is a dataset):

this.dataSet1.Tables[0].DefaultView.AllowNew = false;

and you get to keep the dataset or datatable as the direct datasource.


Monday, 22 November 2004 17:05:24 (GMT Standard Time, UTC+00:00)  #    Comments [0]  .Net Windows Forms

# Tuesday, 26 October 2004
TextBox.Focus() doesn't work within Form_Load

i have a windows form with a textbox inside a panel, and the following code doesn't work as expected:

private void WizardLoad(object sender, System.EventArgs e)
{
  this.txtUsername.Focus();
}

Thanks to someones post

i now know that the best way to do it is as follows:

this.ActiveControl = this.txtUsername;

It works!


Tuesday, 26 October 2004 12:06:57 (GMT Daylight Time, UTC+01:00)  #    Comments [1]  .Net Windows Forms

# Sunday, 03 October 2004
Complications with textbox and Newline and Carriage Return characters

My windows application uses a multi-line textbox to allow the user to update text, which gets sent to and from a web service, and then into and out of a sql database.  Somewhere along the way, the \r\n characters turn into \n characters.  So when i re-load the string from the web service into the textbox, it gets displayed as block characters instead of proper line breaks.  I wrote the following simple function to work around this issue. 

It firstly removes all the \r characters, which has the effect of changing the \r\n characters to \n. Then it replaces the \n chars to \r\n. 

Note: The reason you can't just replace the \n chars with \r\n is because you would end up with \r\r\n chars if there were \r\n's in the string before you did the replace.

/// 
/// this method formats a string for correct display in a multiline text box.
/// It removes \n characters and replaces them with correct \r\n chars
/// 
public static string FormatForMultiLineTextBox(string s)
{
return s.Replace("\r","").Replace("\n","\r\n");
}

i will also take this opportunity to share a method i use to prevent errors caused by sending certain binary characters across a web service (at least in WSE 2).  The method removes all un-useful binary characters (useful... in terms of my app).

/// 
/// This method removes all binary characters except:
/// Vertical Tab 0x09
/// New Line 0x0A
/// Carraiage Return 0x0D
/// 
public static string StripBinaryChars(string s)
{
    return Regex.Replace(s, @"[\x00-\x08\x0B-\x0C\x0E-\x1F]","");
}

see www.asciitable.com for more information on the hex codes for these binary characters.


Sunday, 03 October 2004 18:40:37 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net Windows Forms

# Wednesday, 21 July 2004
Some issues with PureComponents Navigator control 2.0

I have built a content management system in windows forms, and i set it up with individual forms to manage each of the aspects to the system. one to manage users, another for templates etc.  however this made the user interface quite cluttered sometimes with several windows open.  so i decided to make my app work like outlook 2003 with its nice control on the left with buttons and side panels on the left, and the main panel on the right for whichever button is selected. 

i chose the pure components navigator control (2.0) and i'm testing it at the moment.  i don't like the way the developers don't give a public support forum, they say it shouldn't need support, but i have had several issues with it.

the main problem i have is that the control seems to crash Visual Studio (2003 enterprise edition) when i add panels to each group and set properties such as anchoring in the designer.  i ended up cutting the code to set up the panels from InitialiseComponent and pasting it into a separate function that i call in the constructor.

also, the group class is supposed to have a FitPanel() method, but it doesn't fill the panel to the size of the space in the control between the buttons.  i tried setting the panel to DockStyle.Fill and it fills the entire control on top of all the other buttons.  I ended up manually creating the panels (right click a group and Add Panel) and then resize it to the desired dimensions, and set the anchor to top|left|bottom|right.  i must say it was quite messy though. 

it seems like a great control and it works quick enough too. i'll need to do more testing before i pay for the license though.


Wednesday, 21 July 2004 23:58:51 (GMT Daylight Time, UTC+01:00)  #    Comments [2]  .Net Windows Forms

# Monday, 12 July 2004
System tray icons in Windows forms apps

If you add a NotifyIcon (system tray icon) to a Windows forms application, you will need to modify the dispose event of the form containing the icon, to explicitly call dispose on the notifyIcon as follows:

protected override void Dispose( bool disposing )
{
 if( disposing )
 {
  this.notifyIcon1.Dispose();
  if (components != null)
  {
   components.Dispose();
  }
 }
 base.Dispose( disposing );
}

if you don't do this, the icon will get left in the system tray after the application quits, until you move the mouse over the icon and then it will disappear.


Monday, 12 July 2004 10:02:42 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net Windows Forms

# Tuesday, 22 June 2004
Adventures with .NET smart clients, running with Local Intranet permissions

I had originally deployed an app with msi but the advantages of smart clients soon began to convince me to change my deployment model.

What you can and can't do in Local Intranet

As a smart client, the app is executed as http://myserver/exec/myApp.exe
This works very nicely, until those security exceptions start appearing.  I had to spend a long time going through my app to find out what was causing the exceptions.  The obvious reason for the exceptions is that programs coming in from the Intranet are less trusted than those on My Computer, and hence are subject to more restrictions by the local security policy.  After much exploration and reading up online, i found out some interesting facts:

  • you can't access Environment.Machinename
  • you can't access My Documents, or UserAppDataPath, etc.
  • you can't call abort() on a Thread, but you can call Sleep() and Start()
  • you can read files through openFileDialog
  • you can read and write to IsolatedStorage (a virtual file-system specially for your app), very useful seeing as you can't write to any physical location on the hard disk by default.
  • you can invoke delegates asynchronously

You could deduce these by looking in the .Net Framework Security Policy (control panel > admin tools > .Net Framework configuration > Runtime security policy > Machine > Permission Sets > Local Intranet), and then double click one of the items in the main pane to the right, to see what restrictions if any are applied for the selected zone.

I found out the above information hard way through trial and error, but it was a very valuable exercise to learn about the security policy.

Using a class library with your smart client app

I got a large part of my app working, but i was using a class library of customised windows forms components, and this was causing a security exception.  It took a few hours to pin down, but the docs finally revealed that the class library must have the following attribute (outside a class declaration):

[assembly:AllowPartiallyTrustedCallers] 

The app worked fine after I added that line.

Requesting permissions from the local Security Policy before your app runs. 

If you are not sure what zone your app will run in, you can request a minimum level which your app requires. Add the following line of code outside a class declaration in one of the files in your project:

[assembly:PermissionSetAttribute(SecurityAction.RequestMinimum, Name = "LocalIntranet")]

This will alert the user that they don't have permissions to run the app if it is running in a zone less than Local Intranet. 

Conclusion

I personally have found these bits of information very difficult to pull together.  There is a ton of stuff about smart clients but its mostly non-technical.  I even got a CD from msdn about smart clients and i found it useless for helping out with any of the problems i have encountered along the way.

I hear there is a tool called PermCalc.exe coming with Whidbey that will probe an app to see what would cause security problems in a smart client scenario but until then, we are stuck doing it manually :)


Tuesday, 22 June 2004 19:16:34 (GMT Daylight Time, UTC+01:00)  #    Comments [0]  .Net Windows Forms

# Monday, 24 May 2004
Environment.SpecialFolder enumeration not showing up in Visual Studio Intellisense

I was using an OpenFileDialog and i wanted it to open at the My Pictures folder for the current user.  So i looked up the msdn docs for how to locate this folder, and i come across the Environment.SpecialFolder enumeration, which has an entry for MyPictures.  Great, my code then was:

this.openFileDialog1.InitialDirectory = Environment.SpecialFolder.MyPictures; 

but this didn't compile, and it didn't show up in Intellisense.  I read a few examples online which used the following syntax:

this.openFileDialog1.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures); 

and it worked, although it still didn't show up in Intellisense.  you'd think MS would at least explain this in the docs, weird.


Monday, 24 May 2004 12:34:15 (GMT Daylight Time, UTC+01:00)  #    Comments [1]  .Net Windows Forms

# Monday, 15 March 2004
Windows Forms DataGrid throwing a null reference exception on DataRowView.CancelEdit()

I encountered a frustrating issue using the Windows forms Datagrid today.  This problem was also posted without reply on an ms newsgroup. I'm posting my work around here for the record. 

Problem Description
My windows form had a tabcontrol, with 2 tab pages, each with a datagrid, bound to a table in a dataset.  The problem occurred when the user switched from the second tab to the first, with the last row of the datagrid being left as a new row with default null values. It was in 'edit mode' usually because the user added a new row, or pressed enter, which automatically places the cursor in another empty new row, with the default values. The exception I got was generated by the framework and not from my code, as follows:

Object reference not set to instance of an object, at:

System.Data.DataRowView.CancelEdit() at
System.Windows.Forms.CurrencyManager.CancelCurrentEdit() at
System.Windows.Forms.DataGrid.OnLeave_Grid() at System.Windows.Forms.Control.InvokeMarshaledCallbacks()

The exception is interesting in that the CurrencyManager kicks in when the focus of the DataGrid has left, and cancels the cell currently being edited. 

Solution
I tried several solutions, and eventually worked around the problem by forcing the user to click a button to switch from one tab to the other (I hid the tab change buttons off the screen), and then disabling whichever grid wasn't being used after the button was pressed.  This seemed to change the way the DataGrid cancelled the edit of the last row, and the exception doesn't happen now.

It's not an elegant solution, so I'm open to suggestions. I do think the datagrid should be able to execute CancelCurrentEdit without throwing exceptions, and I would be interested to understand why it happens.  I guess it will be fixed in .Net version 2.


Monday, 15 March 2004 19:12:33 (GMT Standard Time, UTC+00:00)  #    Comments [2]  .Net Windows Forms