Rules to Better Windows Forms Applications - 52 Rules
Elevate your Windows Forms applications with a set of best practices designed to enhance user experience and maintainability. This guide covers key principles for design consistency, error logging, user settings management, and efficient data handling, ensuring your applications are both functional and user-friendly.
Almost everyone assumes today to use web forms for broad reach because of easy installation and cross platform compatibility. That is correct.
In the old days (1995-2000) companies used Windows Forms, later (2000-2007) they rolled their own ASP.NET solution, however since then (2007+) SharePoint has become the default choice for an intranet. When you need something richer and you can control the environment.
- Bandwidth - Presentation Layer
Only the data is transferred from the server, not the presentation code. Web forms must download the data and the rendered UI taking up large bandwidth. - Bandwidth - Compression
Data transfer can be compressed and uncompressed to use less bandwidth.
For example, using a Pkzip scale (1-9) of 6, we used the Open source algorithm 'Blowfish' to compress/encrypt 240K of data to 30K. i.e. 87% compression. - Caching
If you are going to the same record within a certain time period, Windows forms will retrieve the data from cache instead of calling the data service again.
For example, when you click search on a Windows form, you don't have to do a request again if the search was done recently. - Faster Server
Because of the bandwidth advantages above, the server will make less requests and hence runs faster. The client has become thicker, using more processing power and capable of more complex business logic. - Richer Interface
The application's interface can be richer as you can design your own custom controls and do not need complicated resource-intensive and complex DHTML and JavaScript. - More Responsive
The interface will respond quicker to your clicks, no need to post a request for an interface response. i.e. no 10 second latency. - Better Development
Development is much easier with quick feedback. There are no compliance issues to follow as in web development with browsers. - More people are happy!
By choosing windows forms you are making the developer, end user and accounts groups happier. The only group which may rather a Web solution is the network admins.
Group Browser Based Rich Client Network Admins ✅ ❌ Developers ❌ ✅ End Users ❌ ✅ Accounts ❌ ✅ Figure: Table of who benefits from Windows Forms, and Web Forms
- Bandwidth - Presentation Layer
Code generators can be used to generate whole Windows and Web interfaces, as well as data access layers and frameworks for business layers, making them an excellent time saver. It's not crucial which one you use as long as you invest the time and find one you are happy with. The one important thing is they must have command line support and the files they generate should be recognizable as code generated by prefix or a comment like "Don't touch" as this was automatically generated code. Make it easy to run by putting all the command line operations in a file called '_Regenerate.bat'.
A
Regenerate.bat
file must exist under the solution items to recreate data access layer and stored procs.The built in Data Form Wizard in Visual Studio .NET is not any good. We prefer other code generators like CodeSmith - Good for generating strongly-typed collections.
Note: It also includes templates for Rocky Lhotka's CSLA architecture from a SQL Server database.
Use colours on incomplete is so useful in design time:
- Red = Controls which are incomplete, e.g. An incomplete button
- Yellow = Controls which are deliberately invisible that are used by developers e.g. Test buttons
Usually these controls are always yellow. However sometimes new areas on forms are made red and visible, so you can get UI feedback on your prototypes. Since they are red, the testers know not to report this unfinished work as a bug.
All applications should be compatible with the Windows XP user interface and should be fully themed. Applications that do not use XP themes look like they were designed only for an earlier version of Windows. Mixing themed and non-themed controls looks equally unprofessional.
Implementing XP Themes
We recommend using manifest file to support XP Themes in .NET. Follow this to use the manifest file.
-
Set the FlatStyle Property in all our controls to "System"
- Copy XPThemes.manifest file to your bin folder
By default, you can get it fromC:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\XPThemes.manifest
- Rename "XpThemes.manifest" to "ApplicationName.exe.manifest"
Note: In .NET 1.1 you can use Application.EnableVisualStyles to support XP Themes. This approach is not recommended because it can cause an 'SEHException' to be thrown and some common controls could disappear.
-
If you ask a new .NET developer (from the Access or VB6 world) what is the best thing about .NET Windows Forms, most of your answers will be "Form Inheritance" that allows them to keep a nice consistent look for all forms. If you ask them a couple of months later, they will probably tell you the worst thing about .NET Windows Forms is "Form Inheritance". This is because they have had too many problems with the bugs in the form designer regarding this feature. Many abandon them altogether and jump on the user control band wagon. Please don't... we have a solution to this...
If you can keep the level of form inheritance to a minimum, then you may not see the problem or at least you will experience the problem less. Anyway even if you do, stop whinging and just close down Visual Studio.NET and restart. You don't change the base form that often anyway.
Well how do you keep it to a minimum? Well make the first base form without any controls, only code (to make it as flexible as possible and avoid having a multitude of base forms).
We try to keep the number of controls on inherited forms, and the levels of inheritance to a minimum, because it reduces the risk of problems with the Visual Studio Designer (you know when the controls start jumping around, or disappearing from the Designer, or properties getting reset on inherited copies or even the tab order getting corrupted). Designer errors can also occur in the task list if the InitializeComponent method fails.
Every form in your application should inherit from a base form which has code common to every form, for example:
- Company Icon
- Remembering its size and location - Code sample to come in the SSW .NET Toolkit
- Adding itself to a global forms collection if SDI (to find forms that are already open, or to close all open forms)
- Logging usage frequency and performance of forms (load time)
a) Sorting out the StartPosition:
- CentreParent only for modal dialogs (to prevent multi-monitor confusion)
- CentreScreen only for the main form (MainForm), or a splash screen
- WindowsDefaultLocation for everything else (99% of forms) - prevents windows from appearing on top of one another
b) Sorting out FormBorderStyle:
- FixedDialog only for modal dialog boxes
- FixedSingle only for the the main form (MainForm) - FixedSingle has an icon whereas FixedDialog doesn't
- None for splash screen
- Sizable for everything else (99% of forms) - almost all forms in an app should be resizable
We have a program called SSW CodeAuditor to check for this rule.
c) Sorting out a base data entry form:
- Inherited from the original base form
- OK, Apply and Cancel buttons
- Menu control
- Toolbar with New, Search and Delete
Note: The data entry base form has no heading - we simply use the Title Bar.
We have a program called SSW .NET Toolkit that implements inherited forms.
When you have a link in your application, use the same text layout as below and a "More" hyperlink to the same page with the same description. The resulting effect is when the user clicks on the "More" hyperlink, the page will begin with exactly the same information again. This ensures the user is never confused when navigating from your application to a link.
One useful feature of inherited forms is the ability to lock the value of certain properties on the inherited copy. E.g.:
- Font - we want to maintain a consistent font across all forms
- BackColor - changing the background color prevents the form from being themed
- Icon - we want all of our forms to have the company Icon
This can be achieved with the following code, which works by hiding the existing property from the designer using the Browsable attribute. The Browsable attribute set to False means "don't show in the the designer". There is also an attribute called EditorBrowsable, which hides the property from intellisense.
C#:
using System.ComponentModel; [Browsable(false)] // Browsable = show property in the Designer public new Font Font { get { return base.Font; } set { //base.Font = value; //normal property syntax base.Font = new Font("Tahoma", 8.25); // Must be hard coded - cannot use Me. } }
VB.NET:
Imports System.ComponentModel <Browsable(False)> _ Public Shadows Property Font() As Font Get Return MyBase.Font End Get Set(ByVal Value As Font) 'MyBase.Font = Value 'normal property syntax MyBase.Font = Me.Font End Set End Property
User controls allow you to have groups of elements which can be placed on forms.
❌ Bad: User controls can be really misused and placed in forms where they shouldn't be. An example of that is shown below, under the components directory the user controls placed and used only once at a time during the application flow. There is much more coding responsibility on the developer to load those controls correctly one at a time inside the main form.
✅ Good: User Controls are best used for recurring or shared logic either on the same form or throughout the application. This encourages code reuse, resulting in less overall development time (especially in maintenance). Example, the figure below shows the good use of User Controls, the address control is repeated three times but coded once.
Exception: User controls can be made for tab pages (e.g Tools | Options) and search pages. This allows the breakdown of complex forms, and development by different developers.
Summary
✅ The pros of User Controls are:
- You can use a user control more than once on the same form eg. Mailing Address, Billing Address
- You can reuse logic in the code behind the controls e.g. Search control
- User controls are less prone to visual inheritance errors
- When used in a form with multiple tab pages - and each tab page potentially having a lot of controls, it is possible to put each tabpage into a separate user control
- Reduce lines of generated code in the designer by splitting it into multiple files
- Allow multiple persons to work on different complex tabpages
❌ However the cons are:
- You lose the AcceptButton and CancelButton properties from the Designer eg. OK, Cancel, Apply. Therefore the OK, Cancel and Apply buttons cannot be on User Controls
Designing a user-friendly search system is crucial in today’s information-driven world, as it significantly enhances the user experience by enabling efficient and effective access to relevant data. A well-structured search interface not only simplifies the process of locating specific information amidst vast datasets but also caters to a variety of user needs, from basic inquiries to complex queries.
By prioritizing clarity, simplicity, and adaptability in search design, we can ensure that users can navigate and utilize applications more intuitively, leading to increased productivity, satisfaction, and overall success of the software.
Therefore, I believe search system should:
- Importatnt - Separate it from the data entry fields (on a different form) - this avoids confusion
- Have a "Simple" tab this shows minimum fields, that is just one like Google.
E.g. A customer calls, they said they were from Winkleton, but I'm not sure what that is. Do I put it in the Region, City or Address fields? so you need to simply search in all fields with one single text box. - Have a "Recent" tab this shows the most recent records opened/updated
- Have a "Common" tab this shows the common fields
Note: Preferred over customers needing to learn prefixes like Google (for example, "city:winkleton"). - Have an "Advanced" tab only for power users for building up a WHERE clause
We have a program called SSW .NET Toolkit that implements this cool Search Control.
Validation is extremely important on a data entry form. There are two ways to do validation:
- ErrorProvider control
The ErrorProvider control is code intensive. You must manually handle the Validating event of each control you want to validate, in addition to manually running the validation methods when the OK or Apply button is clicked.
Private Sub productNameTextBox_Validating(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) Handles _ productNameTextBox.Validating ValidateProductName(False) End Sub Private Function ValidateProductName(ByVal force As Boolean) _ As Boolean If Me.productNameTextBox.Text.Length = 0 Then Me.errorProvider.SetError(Me.productNameTextBox, "You must enter the Product Name.") If force Then MessageBox.Show("You must enter the Product Name.", _ Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Warning) End If Return False Else Me.errorProvider.SetError(Me.productNameTextBox, _ String.Empty) Return True End If End Function Private Function ValidateInput() As Boolean Dim force As Boolean = True Dim isValid As Boolean = ValidateProductID(force) If Not isValid Then force = False End If isValid = ValidateProductName(force) If Not isValid Then force = False End If isValid = ValidateCategory(force) Return isValid End Function Private Sub okButton_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) If Me.ValidateInput() Then 'Test End If End Sub
Figure: Bad example - lots of code but no balloon tooltips
Private Sub productNameTextBox_Validating(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) _ Handles productNameTextBox.Validating ValidateProductName(False) End Sub Private Function ValidateProductName(ByVal force As Boolean) _ As Boolean If Me.productNameTextBox.Text.Length = 0 Then Me.errorProvider.SetError(Me.productNameTextBox, _ "You must enter the Product Name.") If force Then If Me.balloonToolTip.IsSupported Then Me.balloonToolTip.SetToolTip(Me.productNameTextBox, _ "You must enter the Product Name.") Else MessageBox.Show("You must enter the Product Name.", _ Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Warning) End If End If Return False Else Me.errorProvider.SetError(Me.productNameTextBox, _ String.Empty) Return True End If End Function Private Function ValidateInput() As Boolean Dim force As Boolean = True Dim isValid As Boolean = ValidateProductID(force) If Not isValid Then force = False End If isValid = ValidateProductName(force) If Not isValid Then force = False End If isValid = ValidateCategory(force) Return isValid End Function Private Sub okButton_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) If Me.ValidateInput() Then 'Test End If End Sub
Figure: Good example - lots of code but balloon tooltips are used
Note: The component for balloon tooltips can be found in the SSW .NET Toolkit.
The error provider has the advantage over the extended provider that it can be used with balloon tooltips. If you are not using balloon tooltips, however, the error provider should not be used.
- SSW Extended ProviderThe SSW Extended Provider integrates with the ErrorProvider control to provide the same functionality, but requires no code to implement (everything can be done in the Designer).
We have a program called SSW .NET Toolkit that implements this cool Error Provider Control
- ErrorProvider control
In .NET, there are 2 ways to pass data through the layers of your application. You can:
- Use DataSet objects, OR
- Write your own custom business objects
There are 2 very different opinions on this matter amongst .NET developers:
✅ Pros of DataSet object:
- Code Generation - Strongly typed DataSet objects can be created automatically in Visual Studio. Custom business objects must be laboriously coded by hand.
- CRUD functionality DataSets - When used with data adapters, can provide CRUD (Create, Read, Update, Delete) support. You must manually implement this functionality with custom business objects.
- Concurrency - Support for concurrency is part of the DataSet object. Again, you must implement this yourself in a custom business object.
- Data binding - It is difficult and time-consuming to write custom business objects that are compatible with data binding. The DataSet object is designed for data binding.
✅ Pros of Custom Business objects:
- Better performance - The DataSet object is a very heavy object and is memory-intensive. In contrast custom business objects are always much more efficient. Business objects are usually faster when manipulating data, or when custom sorting is required.
- Business objects allow you to combine data storage (NOT data access) and business logic (e.g. validation) in the one class. If you use DataSet objects, these must be in separate classes.
The Case for Business Objects
Usually, it is recommended to choose datasets as it is believed you get more for free. However, all the the features you get in the dataset can be manually coded up in business objects.
E.g. For business objects you must manually code up the bindings, with datasets however you may use the designer for binding straight after designing the dataset. This layer should be code generated - so it doesn't matter much.
In Visual Studio, binding to business objects is supported in which case we might be swayed to use business objects.
Exception: Real complex forms say 500,000 lines of C# code
Datasets are a tool for representing relational data in an object oriented world. They are also slower across networks. Datasets are fantastic for maintenance forms (an editable grid with a couple of checkboxes and text boxes and a save button), but terrible for real complex forms. In a complicated scenario you might have a Customer object. An Order form has a reference to this customer object that it uses to display. When a process is run on the Customer invoked from the Order, you can simply pass a reference to the customer, and if something changes, fire an event back to the Order. If datasets were used, you would be either passing datasets around (which some may say is not very safe, or good OO) or pass an ID around and have the process load the data again.
Also it appears.NET 2.0's BindingList makes binding extremely easy along with IEditableObject. But in most cases, you don't even need to implement these.
Rocky Lhotka appeared on a .NET Rocks! episode and they had a big discussion of Business Objects versus DataSets. The use of either must change on a case by case basis. Datasets do allow you to get more for free, but if one day management decide you need to do something a little out of the ordinary, there will be problems. In contrast, business objects take longer to write (this can be minimized with a good code generator and custom templates), but stand the test of time much better than Datasets.
Every form should have a StatusBar that shows the time taken to load the form.
Developers can't catch and reproduce every performance issue in the testing environment, but when users complain about performance they can send a screenshot (which would including the time to load). Users themselves also would want to monitor the performance of the application. This is one of Microsoft Internet Explorer's most appalling missing feature, the status bar only says 'Done.' when the page is loaded - 'Done: Load Time 14 seconds'.
In the figure below, the time taken to load the form over a dialup connection is 61.1 seconds, this proves to the developer that the form is not useable over a dialup connection. In this particular case, the developer has called a 'select * from Employees' where it was not needed, only the name, password and ID is needed for this form.
Note: Once the form is loaded and load time is shown, the status bar can be used to show anything useful as the form is being used.
Add a StatusBar to the form, and add a StatusBarPanel to the StatusBar, then set the properties like below.
private DateTime StartLoadTime = System.DateTime.Now; private void Form1_Load(object sender, System.EventArgs e) { TimeSpan elapsedLoadTime = DateTime.Now.Subtract(StartLoadTime); this.statusBarPanel1.Text = string.Format( "Load time: {0} seconds", Math.Round(elapsedLoadTime.TotalSeconds, 1)); }
To avoid unnecessary database look-ups, many developers cache lookup tables when creating a windows application. There are issue that can arise as a result, mainly to do with the synching of the lookup data. If the database administrator decides to change the lookup tables, there is bound to be a user online using a static old version of the lookup data. This may result in sql exception, and data corruption.
Exception #1: If the application can be taken offline where the users will not access the database for a finite time, then it is recommended that you cache lookup data. However, we do not recommend caching of non-lookup data, i.e. products, clients or invoices.
Note: This is a different scenario to complete offline caching; offline caching is recommended and should be implemented (e.g outlook & IE - [Work Offline].
However, this rule is about combo boxes and list views which contain less than 100 records. There is not much benefit to caching lookup data as there is much more coding involved.Exception #2: If the application contains minor non-critical data. (eg. If you allow the user to customize the text displayed on forms (some people prefer 'Customer' while some prefer 'Client') and this is stored in a database)
Depending on the frequency of this data being changed (and if the change is user dependant), you may want to:
- Low frequency: Place an option to change this data in the application's installation process
- High frequency: Cache the data and provide an option to refresh all cached data or disable caching all together. (e.g menu items View->'Refresh All' and Tools->'Options'->'Disable Caching').
We would love to be proved wrong on this rule. We have 1000s of users on some of our applications, we have tried caching lookup data and we ended up with a lot more code containing exception handling and table refreshing than its benefit.
The designer should be used for all GUI design. Controls will be dragged and dropped onto the form and all properties should be set in the designer, e.g.
- Labels, TextBoxes and other visual elements
- ErrorProviders
- DataSets (to allow data binding in the designer)
Things that do not belong in the designer:
- Connections
- Commands
- DataAdapters
However, and DataAdapter objects should not be dragged onto forms, as they belong in the business tier. Strongly typed DataSet objects should be in the designer as they are simply passed to the business layer. Avoid writing code for properties that can be set in the designer.
Basic data binding should always be done in the designer because the syntax for data binding is complex, and confusing for other developers reading the code.
When you need to handle the Format or binding events, you can still use designer data binding, as long as you hook in your events prior to filling data.
private void Form1_Load(object sender, System.EventArgs e) { Binding currencyBinding = this.textBox1.DataBindings("Text"); currencyBinding.Format += new ConvertEventHandler(currencyBinding_Format); currencyBinding.Parse += new ConvertEventHandler(currencyBinding_Parse); OrderDetailsService.Instance.GetAll(Me.OrderDetailsDataSet1); } private void currencyBinding_Format(object sender, ConvertEventArgs e) { if(e.DesiredType == typeof(string)) { e.Value = ((decimal)e.Value).ToString("c"); } } private void currencyBinding_Parse(object sender, ConvertEventArgs e) { if(e.DesiredType == typeof(decimal)) { e.Value = Decimal.Parse(e.Value.ToString(), System.Globalization.NumberStyles.Currency); } }
// // Designer auto generated code. // private void InitializeComponent() { this.cmbTumorQuad = new System.Windows.Forms.ComboBox(); // // cmbTumorQuad // this.requiredValidator1.SetCustomValidationEnabled(this.cmbTumorQuad, true); this.cmbTumorQuad.DataBindings.Add(new System.Windows.Forms.Binding("SelectedValue", this.dvOccMain, "TumorQuadrant")); this.cmbTumorQuad.DataSource = this.dvTumorQuad; this.cmbTumorQuad.DisplayMember = "Description"; this.requiredValidator1.SetDisplayName(this.cmbTumorQuad, ""); }
Figure: Good example - DataBinding in Designer
private void DataBind() { ChangeBinding(txtRuleName.DataBindings, "Text", jobRules, "RuleData.RuleName"); ChangeBinding(cmbFileFilter.DataBindings, "Text", jobRules, "RuleData.FileFilter"); ChangeBinding(txtSearchString.DataBindings, "Text", jobRules, "RuleData.SearchString"); ChangeBinding(txtCreatedBy.DataBindings, "Text" , jobRules, "RuleData.EmpCreated"); } protected Binding ChangeBinding(ControlBindingsCollection bindings, string propertyName, object dataSource, string dataMember, ConvertEventHandler eFormat, ConvertEventHandler eParse) { Binding b = bindings[propertyName]; if ( b != null ) bindings.Remove(b); b = new Binding(propertyName, dataSource, dataMember); bindings.Add(b); return b; }
Figure: Bad example - DataBinding in Code
private void DataBind() { //Header picRuleType.Image = Core.GetRuleTypeImage((RuleType)rule.RuleType, 48); ruleNameTextBox.Text = rule.RuleName; //General Tab notesTextBox.Text = rule.RuleDescription; ruleUrlTextBox.Text = rule.RuleURL; //Search Tab cboRuleType.SelectedValue = (RuleType)rule.RuleType; searchForTextBox.Text = rule.SearchString; shouldExistComboBox.SelectedIndex = (rule.ShouldExist == true ? 0 : 1); //Change History Tab createdByTextBox.Text = rule.EmpCreated; dateCreatedTextBox.Text = rule.DateCreated.ToString(); lastUpdatedByTextBox.Text = rule.EmpUpdated; dateLastUpdatedTextBox.Text = rule.DateUpdated.ToString(); }
Figure: Bad example - Set controls' values in Code
MDI (Multiple Document Interface) forms should be avoided in most modern data-centric applications because they:
- Are a hangover from the days of Windows 3.1 and Access 2.0
- Constrained within a smaller window
- Only show as one window on the taskbar
- Have no multiple monitor support (the killer reason)
::: good
Me.IsMdiContainer = true; ClientForm frm = new ClientForm(); frm.MdiParent = this; frm.Show();
Figure: Bad code example - using MDI forms
ClientForm frm = new ClientForm(); frm.Show();
Figure: Good example - not using MDI
MDI forms have the advantage that the MDI parent form will have a collection MdiChildren which contains all of its child forms. This makes it very easy to find out which forms are already open, and to give these forms focus. Accomplishing this with an SDI application requires you to:
- A global collection of forms
- A line of code on the load and closed events of each form which adds / removes the form from the global collection
But what about tabs? As developers, we love to use tabs similar Visual Studio.NET (figure below) and browsers such as Mozilla and CrazyBrowser. Tabs are great for developers, but standard business applications (e.g Sales Order System) should be developed as SDI (Single Document Interface). This is because users are used to Outlook and other office applications, which don't use MDIs at all. If the users want to group windows, Windows XP lets you "Group Similar Taskbar Icons".
Your common code assembly should be divided into the following sections:
-
Common (e.g. SSW.Framework.Common)
- Code which is not UI specific
- Example: Code to convert a date into different formats
-
CommonWindows (e.g. SSW.Framework.WindowsUI)
- Example: Base forms which are the same for all products, wizard frameworks
-
CommonWeb (e.g. SSW.Framework.WebUI)
- Example: Generic XML-based navigation components
For more information see Do you have a consistent .NET Solution Structure?.
-
Data Access Layers should support not only direct connections to SQL Server but also connections through web services.
Many applications are designed for use with a database connection only. As users decide to take the application some where else away from the database, the need for web services arises.
There are 3 ways to implement this:
- Lots of if statements (really messy - most people try this first)
- Interfaces (implements statement in VB)
- Factory pattern (✅ best - most flexible and extensible approach)
All database applications should be web services ready as the future direction is to use web services only, because even locally a web service connection is not much slower than direct connection. The performance difference shouldn't be substantial enough to require a double code base.
All unhandled exceptions should be logged to provide developers with sufficient information to fix bugs when they occur. There are two options we for logging exceptions:
The Microsoft Exception Management Application BlockMicrosoft provides full source code for the EMAB, which is fully extensible with custom logging target extensions. We decided to customize the EMAB to produce the SSW Exception Management Block, which logs exceptions to a database using a web service, allowing us to keep a history of all exceptions.
Your code should not contain any empty catch blocks as this can hamper exception handling and debugging.
We have a program called SSW Code Auditor to check for this rule.
We have a program called SSW .NET Toolkit that implements Exception Logging and Handling
By using logging, the developer has access to more information when a particular error occurs like which functions were called, what state is the application currently in and what certain variables are. This is important as a simple stack trace will only tell you where the error occurred but not how it occurred.
Log4Net is an open-source logging library for .NET based on the Log4J library. It provides a simple to use library to enable logging in your application. It provides several logging options such as:
- XML File (Recommended)
- Text File
- Database
- Rolling log file
- Console
Log4Net also provides different levels of tracing - from INFO to DEBUG to ERROR - and allows you to easily change the logging level (through the config file)
We have a program called SSW CodeAuditor to check for this rule.
If your application accesses properties in app.config, you should provide a strongly typed wrapper for the app.config file. The following code shows you how to build a simple wrapper for app.config in an AssemblyConfiguration class:
using System; using System.Configuration; namespace SSW.Northwind.WindowsUI { public sealed class AssemblyConfiguration { // Prevent the class from being constructed private AssemblyConfiguration() { } public static string ConnectionString { get { return ConfigurationSettings.AppSettings["ConnectionString"]. ToString(); } } } }
Unfortunately, the Configuration Block does not automatically provide this wrapper.
In Visual Studio 2003 the standard DataGrid has some limitations. It was ugly compared to a ListView and did not support combo box or button columns, making it useless for many applications.
In Visual Studio 2005 we have this great new DataGridView control which solves these problems.
If you still want more then you need a 3rd party control. We recommend these (in this order):
- Janus GridEx
- Developer Express XtraGrid
- Infragistics Wingrid
- ComponentOne TrueDBGrid
For more Details have a look at our Best 3rd Party Controls for Windows Forms
A good replacement for the standard Date Time picker is the UltraDatePicker by Infragistics.
The main reason for the use of the UltraDatePicker over the standard .NET one is because the .NET one does not take null for a date value.
This is a lot of hassle for DataBinding. The Windows Form DataBinding will try to put null into the bound field, when:
1. The bound data is DBNull
2. The current row is removed (i.e., there is no more data in the DataTable)
If you set the property "Nullable" to false in UltraDatePicker, the same issues appears again.
So the solution is to allow null, but where the field is required, make sure the validation picks it up and asks the user to enter a value when saving the form.
The menu & toolbar controls in Visual Studio .NET 2003 do not allow you to have icons in your menus or have alpha-blended toolbar icons. They also do not provide an Office 2003 like look. However, we have tried several third party menu and toolbar controls and all of them had serious bugs. E.g.:
-
DotNetMagic
- Docking panels didn't implement enough events and it is unclear what the events are doing
- Menu control is OK
- DotNetBar
- Janus Systems
We love 3rd party controls, a lot of developers spend a lot of time implementing these tools to make their applications sweeter, but we found that there is not enough benefit in implementing these controls.
I am very keen on 3rd party controls, but only where they add real value. Knowing about Visual Studio 2005 which provides Office 2003 style menus and toolbars with the new ToolStrip control mean I will wait in this case....Another worry is upgrading from these 3rd party controls will be difficult)
However, it would be better if VS 2005 stored the details of menus and toolbars in an XML file.
-
List Views such as in SSW Diagnostics can present a wealth of information to the user. But too often, users are unable to copy this information to paste into a support email because the list view doesn't support copying. Instead, the user has to frustratingly retype the information with the risk of introducing errors.
Make it easier for the user by enabling the "MultiSelection" property of a ListView and providing a right click menu with a "Copy" item that copies to the clipboard.
Opening a specific web page (that the user is aware of) from a windows application should always be in the form of a hyperlink. Below is a simple example of a hyperlink simply opening a web page containing just more information or help.
However if you are taking action then opening the page (e.g concatenating the URL, etc) then you must have an image button to illustrate the action which will be taken.
Here is a compilation of a few bad examples for this:
But when it requires some form of action (e.g. generating reports, passing and processing values), use a button with an image.
Note: Screenshot contains XP button because the .Net 1.1 button does not support images, however the default button in .NET 2.0 supports images. E.g.
EdwardForgacs.Components.WindowsUI.dll
If you have a button in a form you must have an accept or a cancel button. As a result user can use "Enter" and "Esc" to control the form.
We have a program called SSW CodeAuditor to check for this rule.
Note: The CodeAuditor Rule will just test the buttons on the Base form and ignore all the inherit forms, because for more reusable code, the Accept and Cancel buttons should be in the base form.
If your form has an "OK" button it should be renamed to be as an action verb. For example: Save, Open, etc.
We have a program called SSW Code Auditor to check for this rule.
If you have a multi-line textbox in a form, you should make the "Enter" key go to the next line in the text box, rather than cause it to hit the OK button.
It can be done by assigning "True" value to AcceptsReturn and Multiline options in properties bar.
There are a few common controls we always use in our products. For example, DateTime and Ellipsis Button. We need a standard for the width so the controls should be more consistent.
Note: Controls on base forms will be made to be 'protected' rather than 'private', especially so that inherited forms of different sizes don't mess up.
We have a program called SSW Code Auditor to check for the following two rules:
Rule - C#/VB.NET UI- Button Height and Width - for Standard Button (75 x 23 pixels)
- Level 2: All buttons < 6 characters:** Check the standard size (75 X 23 pixels) for buttons with the word length less than or equal to six characters, except the following buttons.
-
Level 1: The action buttons:** Check the standard size (75 X 23 pixels) for the following action buttons:
- Add
- Delete
- Edit
- OK
- Close
- Cancel
- Save
- Browse
- Select
- Test<
- Next
- Back
- Remove
- Refresh (Exception to the rule as it has 7 letters)
Aside from ease of installation, what is the one thing a web browsers has over a Windows Forms application? - a URL!
With a Windows Forms application, you typically have to wade through layers of menus and options to find a particular record or "page". However, Outlook has a unique feature which allows you to jump to a folder or item directly from the command line.
We believe that all applications should have this capability. You can add it to a Windows Application using the following procedure:
- Add the necessary registry keys for the application
- HKEYCLASSESROOT\AppName\URL Protocol = ""
- HKEYCLASSESROOT\AppName\Default Value = "URL:Outlook Folders"
- HKEYCLASSESROOT\AppName\shell\Default Value = "open"
- HKEYCLASSESROOT\AppName\shell\open\command\Default Value = "Path\AssemblyName.exe /select %1"
- Add code into your main method to handle the extra parameters
C#:
public static void Main(string[] args) { ... if(args.Length > 0) { string commandData = args[1].Substring(args[1].IndexOf(":") + 1).Replace("\"", String.Empty); Form requestedForm = null; switch(commandData) { case "Client": { requestedForm = new ClientForm(); break; } // Handle other values default: // Command line parameter is invalid { MessageBox.Show("The command line parameter specified" + " was invalid.", "SSW Demo App", MessageBoxButtons.OK, MessageBoxIcon.Error); // Exit the application return; } } requestedForm.Show(); // Show the main form as well MainForm mainForm = new MainForm(); mainForm.Show(); // Give the requested form focus requestedForm.Focus(); Application.Run(mainForm); } else // No command line parameters { // Just show the main form Application.Run(new MainForm()); } }
VB.NET:
Public Shared Sub Main() ...
Dim args As String = Microsoft.VisualBasic.Command() If args.Length > 0 Dim commandData As String = _ args.Substring(args.IndexOf(":") + 1).Replace("""", "") Dim requestedForm As Form = Nothing Select Case commandData Case "Client" requestedForm = New ClientForm() ' Handle other values Case Else ' Command line parameter is invalid MessageBox.Show("The command line parameter specified " &_ "was invalid.", "SSW Demo App", MessageBoxButtons.OK, &_ MessageBoxIcon.Error); ' Exit the application Exit Sub End Select requestedForm.Show() ' Show the main form as well Dim mainForm As MainForm = New MainForm() mainForm.Show() ' Give the requested form focus requestedForm.Focus() Application.Run(mainForm); Else ' No command line parameters, just show the main form Application.Run(new MainForm()) End If End Sub
Sample code implementation in the SSW .NET Toolkit
Following on from including a URL, almost every form should have a Back and an Undo button which takes you back to the previous screen, or reverses the last action. This is just like Outlook (see figure below), it has a Back button to take you to the previous folder and an Undo button.
Notes:
- "Back" button should only be implemented if different views can be shown in the same window
- Don't put "Undo" buttons on non data entry forms such as a Print Preview form
The list of forms/URLs and the order in which they have been accessed should be stored in a DataSet held in memory (like IE) - not saved to disk.
For example:
Menu Action Undo Back Cut Remember: Remember Text and Cursor Position
Cut To ClipboardReturn to Remember n/a Save Record Remember old values
Execute procCustomerSave
Close FormReturn to Old values Reopen form Sample code implementation in the SSW .NET Toolkit.
There should always be default values in your application if you allow users to change the settings. This will help your users to have a better first time experience and insure the application work as expected.
However when the users change settings for their own preference, it is better to save these settings and give user has a better return experience, your application looks smarter in this way.
In development life cycle, developers always have different settings to the user's settings. Because of this, debug settings won't always work on the remote machine.
In order to have settings.config, we also have a defaults.config. This is good because this gives a chance for the user to roll back bad settings without reinstalling the application. The application can also roll back the settings it automatically. Below is the code that what we do.
VB.NET
Public Sub RuneXtremeEmail(ByVal state As Object) If Environment.MachineName <> Configuration.MachineName Then resetSettings() Else End
We have a program called SSW Code Auditor to check for this rule.
We have a program called SSW .NET Toolkit that implements this rule.
Note: in Access we do like this
Private Sub Form_Load() If Nz(DLookup("CurrentComputerName", "ControlLocal", "ID=1"), "") <> CurrentComputerName Then Me.ctlCurrentComputerName.Value = CurrentComputerName Else ...
Threading is not only used to allow a server to process multiple client requests - it could make your user interfaces responsive when your application is performing a long-running process, allowing the user to keep interactive.
:: bad
:::private void Form1_Load(object sender, EventArgs e) { this.ValidateSQLAndCheckVersion();// a long task }
Code: No threading code for long task
private void Page05StorageMechanism_Load(object sender, EventArgs e) { this.rsSetupOk = false; this.databaseSetupOk = false; this.NextButtonState.Enabled = false; CheckDatabase(); CheckReports(); } public void CheckDatabase() { if(sqlConnectionString == null) { OnValidationFinished(false, false); } if(upgradeScriptPath ==null && createScriptPath == null) { OnValidationFinished(false, false); } Thread thread = new Thread(new ThreadStart(this.ValidateSQLAndCheckVersion) ) ; thread.Name = "DBCheckingThread"; thread.Start(); }
Code: Threading code for long task
The height of a text box may need to be twice of the font height to display file name in full.
In some cases, running two instances of an application at the same time may cause unexpected result. See this issue is solved via the code below on SSW Exchange Reporter:
try { Process current = Process.GetCurrentProcess(); Process[] processes = Process.GetProcessesByName( current.ProcessName); if ( processes.Length>1 ) { DialogResult userOption = MessageBox.Show(Application.ProductName + " is already running on this machine. " + Environment.NewLine+Environment.NewLine + "Please click: "+Environment.NewLine+ " - 'Try again' to exit the other instance and try again, or "+Environment.NewLine+ " - 'Cancel' to exit now."+Environment.NewLine, Application.ProductName+" "+(new Version(Application.ProductVersion)).ToString(2), MessageBoxButtons.RetryCancel, MessageBoxIcon.Warning); switch(userOption) { case DialogResult.Cancel: return; case DialogResult.Retry: foreach(Process currProcess in processes) { if ( currProcess.Id != current.Id) { currProcess.Kill(); } } break; } } } catch (Exception ex) { TracingHelper.Trace(null, Loggers.WindowsUILogger, TracingLevels.DEBUG, "Cannot get process information, Excpetion occured.", ex) ; DialogResult result = MessageBox.Show("Exchange Reporter cannot detect process information. This may be caused by disabled 'Performance Counter' on your machine. "+Environment.NewLine+ "In such case, Exchange Reporter cannot ensure there is only one instance running. "+ Environment.NewLine+ "You may continue to run Exchange Reporter, however, please make sure you have only one instance of Exchange Reporter running. "+ Environment.NewLine+ "Multiple instances will cause unexpected behaviour. "+ Environment.NewLine+Environment.NewLine+ "Please click 'OK' to continue, or click 'Cancel' to quit." , Application.ProductName+" "+(new Version(Application.ProductVersion)).ToString(2), MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); if ( result == DialogResult.Cancel) { return; } }
Code: Avoid running two instances of an application
Add a column and show "(customized)" in grid - that is an easier way to know if you have changed from the defaults.
A standard menu item "Check for Updates" should be available in the Help menu. Its function is running SSW Diagnostics to check updates and keep the system up to date easily. More on Do you allow users to check for a new version easily?
Here's the code to run Diagnostics:
System.Diagnostics.Process.Start("http://us.ssw.com.au/ssw/diagnostics/download/SSWDiagnostics.application#SSWDiagnostics.application");
In a Windows application, if you need to send mail, please use a WebService to do this, because using WebService to send emails is safer.You don't need to store the email server configuration in your application.config file, which can be installed on the client and be exposed to someone who could take advantage of it.
SmtpClient client = new SmtpClient(); try { client.Host = "****.***.com.au"; client.Port = 25; client.UseDefaultCredentials = true; MailAddress from = new MailAddress("test@ssw.com.au"); string unrecAddy = "test@ssw.com.au"; MailAddress to = new MailAddress(unrecAddy); MailMessage mailMessage = new MailMessage(from, to); mailMessage.Subject = "aaa"; string strVer = "aaa"; mailMessage.Body = "aaa"; client.Send(mailMessage); } catch(Exception ex) { client.SendAsyncCancel(); MessageBox.Show(ex.ToString()); }
Bad example - Send mail without webservice
Emailer.PubicFunction pf = new EmailWebServices.Emailer.Publiction(); pf.SendMail("Test", textBox3.Text, textBox1.Text, textBox2.Text, false, "", null);
Good example - Send mail use webservice
Always choose a GridView (over a ListBox) because it can have:
- Multiple columns
- Checkboxes in the header of the control, which enables users to easily check or uncheck all items
- Add sub-controls added such as buttons, links, charts, and even customized controls to the Gridview. This means you get unlimited flexibility with the GridView
Sometimes, we need to use .NET wrapper to call Windows built-in forms for implementing special functionalities. For example, calling the Directory Object Picker dialog enables a user to select objects from the Active Directory. Microsoft provides an article and an C++ example on how to calling the Directory Object Picker dialog, and the CodePlex website used to give a .NET version of implementation(C#).
However, all of this implementations only work on x86 platform, and will crash on x64 platform, regarding to this problem, the keynote is to understand the difference of IntPtr in between x64 and x86 platforms.
- In x86 platform, IntPtr = Int32
- In x64 platform, IntPtr = Int64
So, To fix the crash, we should re-write the code below:
DSOP_SCOPE_INIT_INFO[] scopeInitInfo = new DSOP_SCOPE_INIT_INFO[2]; IntPtr refScopeInitInfo = Marshal.AllocHGlobal(Marshal.SizeOf (typeof (DSOP_SCOPE_INIT_INFO)) * 2); Marshal.StructureToPtr (scopeInitInfo[0], refScopeInitInfo,true); Marshal.StructureToPtr(scopeInitInfo[1], (IntPtr)((int)refScopeInitInfo + Marshal.SizeOf(typeof(DSOP_SCOPE_INIT_INFO))), true);
Bad example - The code above always gets crash in x64 platform, because of an integer overflow and result in a segmentation fault in 64 bits.
IntPtr refScopeInitInfo = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(DSOP_SCOPE_INIT_INFO)) * 2); int scopeInitInfoSize = Marshal.SizeOf (typeof (DSOP_SCOPE_INIT_INFO)); int offset = scopeInitInfoSize; IntPtr scopeInitInfo = (IntPtr)(refScopeInitInfo.ToInt64() + offset);
Good example - The Directory Object Picker dialog works on both x64 and x86 platforms well when using the good code above.
If a TextBox has Multiline set to true, then the ScrollBars property should be set to "Both" or at least "Vertical".
We have a program called SSW Code Auditor to check for this rule.
Some applications may need to have administrator right for running the application, e.g. create a file, access system library, etc. It will be an issue for the application to run if UAC is turned on. Below is the step to solve the issue:
- Add App.Manifest into WindowsUI project. It should contain the below code:
<?xml version="1.0" encoding="utf-8"?> <asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <security> <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> <requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> </requestedPrivileges> </security> </trustInfo> </asmv1:assembly>
App.Manifest
- Change the project settings for WindowsUI to use the newly created App.Manifest.
It can be extremely tiresome to have to continually remember to set and unset the wait cursor for an application. If an exception occurs you have to remember to add a try finally block to restore the cursor, or if you popup a message box you must remember to change the cursor first otherwise the user will just sit there thinking the application is busy.
AutoWaitCursor Class automatically monitors the state of an application and sets and restores the cursor according to whether the application is busy or not. All that required are a few lines of setup code and you are done. See this great blog on how to use AutoWaitCursor. If you have a multithreaded application, it won't change the cursor unless the main input thread is blocked. In fact, you can remove all of your cursor setting code everywhere!
You don't want someone hitting a delete button by accident. You don't want a use clicking delete expecting a record to be deleted and 10 are deleted.
Aim to make your delete button red and add the count into button text, so the user will be empowered before hitting that fateful delete button.
It is always good idea to set FirstDayOfWeek property to Monday to initialize it instead of leave it with the dafault value.
It is always good idea to set ShowToday or ShowTodayCircle to true to increase the user experience on MonthCalendar control.
If you want to work with sensitive data on textboxes is always good practice to set PasswordChar to "*".
If you add a multiline textbox in a form, you should add anchoring and/or docking properties to allow it to expand when the form is resized.
We have a program called SSW Code Auditor to check for this rule
If you add a text box in a form you should add anchoring and/or docking properties to allow it to grow as the form widens, but not as it increases in height.
We have a program called SSW Code Auditor to check for this rule
If you add a text box in a form you should add anchoring and/or docking properties to allow it to grow as the form widens, but not as it increases in height.
We have a program called SSW Code Auditor to check for this rule