ClickOnce and WinForms application – tiny tips

The below are few points that we learned during a recent application developed using WinForms and deployed using ClickOnce.

Refracting to use native PC/SC interface

The core functionality of the application was to provide proximity card based attendance system for students in a classroom. For this we were previously using the API provided by HID Global. This caused various dependency issues between Windows operating systems and architectures (32bit vs 64bit), so as part of the refactoring we have removed the dependency and using the PC/SC interface exposed by Windows natively. Of course, this means mucking around a bit in the P/Invoke world (for accessing WinScard.dll related functions), but it provides elegance to the application by eradicating extra baggage of dependency.

Disabling Windows SmartCard Plug and Play policy

From Windows Vista the operating system had a group policy setting where the operating system installed new device driver for every proximity card that was swiped on the card reader. This meant that for every card swipe, the user had to wait for Windows to install the device driver and then go through with the rest of the application logic. As part of our requirements this was not necessary, even so more of a hinderance. We have an internal ERP system which stores the additional identification details for the students and we had to integrate with that ERP for the student attendance marking.

In order to disable the above behaviour of Windows, we needed to disable the SmartCard Plug and Play functionality. The details of this setting is detailed at http://technet.microsoft.com/en-us/library/ff404287(WS.10).aspx. Basically the administrator has to disable the “Turn on Smart Card Plug and Play service” option under Computer Configuration -> Administrative Templates -> Windows Components -> Smart Card in the group policy.

Encrypting part of the settings data

Apart from normally authenticating the service calls from the client application, we wanted to obscure the actual service call endpoint strings stored in the application settings. In order to achieve this, we made use of protection mechanism provided by System.Configuration.ConfigurationSection.SectionInformation.ProtectSection function.

We determined that the application was running for the first time using

1
2
3
4
5
6
if ( ApplicationDeployment.IsNetworkDeployed ) { // make sure the application was deployed over the network
	if ( ApplicationDeployment.CurrentDeployment.IsFirstRun ) { // and this is the first time it is running
	   CheckAndSecureConfig( "applicationSettings/MyApp.Properties.Settings" );
	   CheckAndSecureConfig( "applicationSettings/MyApp.Settings" );
	}
}

and then used the ProtectSection functionality

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private static void CheckAndSecureConfig( string sectionName ) { 
	System.Configuration.Configuration config = ConfigurationManager.OpenExeConfiguration( ConfigurationUserLevel.None );
	ConfigurationSection section = config.GetSection( sectionName );

	if ( section != null && !section.SectionInformation.IsProtected && !section.ElementInformation.IsLocked ) {
		section.SectionInformation.ProtectSection( "DataProtectionConfigurationProvider" );
		section.SectionInformation.ForceSave = true;
		config.Save( ConfigurationSaveMode.Full );
	}
}

this makes sure that the settings are not stored in plain strings for anybody to view.

Manually taking over ClickOnce update check

Due to various internal reasons and ClickOnce not successfully (reliably checking and updating to newer version) we have used a check and update mechanism for updating the version of the deployed application. Without reinventing the wheel we simply used the same architecture of ClickOnce by using a custom update checking form and providing event handlers to the System.Deployment.Application.ApplicationDeployment class.

One more advantage of keep the update manual is we can selectively call the update process only when running the application in network deployed mode. So that when doing normal local debugging this doesn’t interfere with the debugging process.

Addressing multiple threading issues on the UI

We were facing with the following exception whenever we tried copying any text from a DataGridView control:

``

When researching on this issue, we came across Soumitra Mondal’s blog entry on addressing the same error when invoking the OpenFileDialog in another application. And using the same methodology we used a helper class for invoking the new form objects in STA thread.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DialogState {
	public DialogResult result;
	public Form dialog;

	public void ThreadProcShowDialog() {
		result = dialog.ShowDialog();
	}
}

// we can use the below help function to open 
// forms on a STA thread
private DialogResult STAShowDialog( Form frm ) {
	DialogState ds = new DialogState( );

	ds.dialog = frm;

	Thread t = new Thread( ds.ThreadProcShowDialog );
	t.SetApartmentState( ApartmentState.STA );
	t.Start( );
	t.Join( );

	return ds.result;
}

By using the STAShowDialog we successfully loaded the target form object on a STA thread and avoided the above mentioned exception.

Active Directory Authentication

I have blogged about the new System.DirectoryServices.AccountManagement in a past blog entry. The main requirement was that we needed to authenticate Active Directory users without a separate administrative login credential for connecting to the directory service. This was achieved using a elaborate LDAP DirectoryEntry class with a directory path using ldap protocol and such. With .NET 4.0 we simply use System.DirectoryServices.AccountManagement for these purposes.

Managing Audio Volume

The authentication of student entry needed to be confirmed using audible alerts apart from the visual alerts. We also wanted to make sure that the system’s volume was appropriately set so that we were not playing the sounds on muted systems. For this purpose we used CoreAudioAPI for the major weight lifting and a helper class to address our requirements:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
using System;
using System.Runtime.InteropServices;
using CoreAudioApi;

namespace SmartCard {
	public static class VolumeManager {
		[DllImport( "winmm.dll" )]
		public static extern int waveOutGetVolume( IntPtr hwo, out uint dwVolume );

		[DllImport( "winmm.dll" )]
		public static extern int waveOutSetVolume( IntPtr hwo, uint dwVolume );

		static MMDeviceEnumerator DevEnum = null;
		static MMDevice device = null; // device will stay null for workstations which do not have a speaker or headphone plugged in

		static VolumeManager() {

			if( Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major >= 6 ) { // If it is Vista or greater
				DevEnum = new MMDeviceEnumerator();
				try {
					device = DevEnum.GetDefaultAudioEndpoint( EDataFlow.eRender, ERole.eMultimedia );
				} catch ( COMException ) {
					device = null;
				}
			}
		}

		public static ushort GetCurrentVolume() {
			if ( device == null ) {
				return 0;
			}

			if( Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major >= 6 ) {
				return ( ushort ) ( device.AudioEndpointVolume.MasterVolumeLevelScalar * 100 );
			}

			// By the default set the volume to 0
			uint CurrVol = 0;
			// At this point, CurrVol gets assigned the volume
			waveOutGetVolume( IntPtr.Zero, out CurrVol );
			// Calculate the volume
			ushort CalcVol = ( ushort ) ( CurrVol & 0x0000ffff );
			// Get the volume on a scale of 1 to 10 (to fit the trackbar)
			CalcVol = ( ushort ) ( CalcVol / ( ushort.MaxValue / 100 ) );

			return CalcVol;
		}

		public static bool Mute {
			get {
				if ( device != null ) {
					return device.AudioEndpointVolume.Mute;
				}

				return true;
			}
			set {
				if ( device != null ) {
					device.AudioEndpointVolume.Mute = value;
				}
			}
		}
		
		public static void SetVolume( ushort volume ) {
			if ( device == null ) {
				return;
			}
			if( Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major >= 6 ) {
				if( volume > 0 ) {
					Mute = false;
				}
				device.AudioEndpointVolume.MasterVolumeLevelScalar = ( ( float ) volume / 100.0f );
				return;
			}

			// Calculate the volume that's being set
			int NewVolume = ( ( ushort.MaxValue / 100 ) * volume );
						// Set the same volume for both the left and the right channels
			uint NewVolumeAllChannels = ( ( ( uint ) NewVolume & 0x0000ffff ) | ( ( uint ) NewVolume << 16 ) );
			// Set the volume
			waveOutSetVolume( IntPtr.Zero, NewVolumeAllChannels );
		}
	}
}

Disabling close button for WinForms

The final form had few business logic to take care before the application was closed, and to control the exit we had to disable standard close button on the top right of the form. We also wanted the user to be able to minimize the application at will, so we couldn’t simply disable the ControlBox. So, the CreateParams was overridden to achieve the desired effect as follows:

1
2
3
4
5
6
7
8
9
private const int CP_NOCLOSE_BUTTON = 0x200;

protected override CreateParams CreateParams {
	get {
		CreateParams cp = base.CreateParams;
		cp.ClassStyle = cp.ClassStyle | CP_NOCLOSE_BUTTON;
		return cp;
	}
}

tip from http://www.codeproject.com/Articles/20379/Disabling-Close-Button-on-Forms.

That is it for now, I hope the above tips ends up helpful for someone.

Happy coding!

comments powered by Disqus