您的位置:首页 > 移动开发

Rules to Better Windows Forms Applications

2004-10-21 10:38 651 查看
Do you agree with them all? Are we missing some? Email me your tips, thoughts or arguments. Let me know what you think.

1. Do you design a mockup UI first?
2. Do you use code generators?
3. Do you use red and yellow colors to distinguish elements in the designer?
4. Do your applications support XP themes?
5. Do you use inherited forms for consistent behaviour?
6. Do you encapsulate (aka lock) values of forms?
7. Do you know when to use User Controls?
8. Do you know how to design a user friendly search system?
9. Do you use Validator controls?
10. Do you use DataSets or create your own business objects?
11. Do you use the designer for all visual elements?
12. Do you always use the Visual Studio designer for data binding where possible?
13. Do you avoid using MDI forms?
14. Do you have a correctly structured common code assembly?
15. Do you include Exception Logging and Handling?
16. Do you make a strongly-typed wrapper for App.config?
17. Do you replace the standard .NET DataGrid?
18. Do you avoid 3rd party menus & toolbars?
19. Do your Windows Forms applications support URLs?
20. Do you include back & undo buttons on every form?
21. Do you use NUnit to write Unit Tests?
22. Are you Data Access Layers compatible with Web Services?
23. Do you use XP button for opening a web page taking action?

Do you design a mockup UI first?

I've seen so much time wasted on complex documentation. I'm all for detailed analysis of specific business logic, but most user requirements can be encapsulated in screen mock-ups of the application. Nutting down the detail in screen mock-ups with the client brings more ideas that produces a better end product. See Do you make dummy screens before you code?

The database schema should be designed before the mockup UI is started. Code Generation should also be done at this stage.

Do you use code generators?

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. At the simplest level, this can mean using the Data Form Wizard in Visual Studio .NET. Other code generators include:

CodeSmith and
RAD Software NextGeneration

Do you use red and yellow colors to distinguish elements in the designer?

Use colors 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.



Figure: Invisible controls highlighted in yellow, and incomplete items highlighted in red

Do your applications support XP themes?

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. In .NET 1.0, applying themes was difficult because you had to create a manifest file and copy it to the run directory. This is no longer the case.
There are two steps are all you need to do:

Call Application.EnableVisualStyles() at the top of the Main method of the application.
Set the FlatStyle property of each control to System.



Figure: Bad - XP themes are not used





Figure: Good - XP themes are used

Do you use inherited forms for consistent behaviour?

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 I have a solution to this....

I think 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 eventhe 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)



Figure: Base Form for all SSW applications with SSW icon

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 Code Auditor. Rules to come:

CentreParent must be used with FixedDialog
FixedDialog must be used with CentreParent
Only one or two forms with CentreScreen
CentreScreen must be used with FixedSingle
FixedSingle must be used with CentreScreen
Only one Form with FormBorderStyle = None
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



Figure: Base data entry form with menu, toolbar and OK, Cancel & Apply buttons

Note: The data entry base form has no heading - we simply use the Title Bar

Do you encapsulate (aka lock) values of forms?

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


Figure: Font Property Visible



Figure: Font Property Hidden

Do you know when to use User Controls?

User controls allow you to have groups of elements which can be placed on forms.



Figure: Good - the Address User Control is repeated

Pros:

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 seperate usercontrol
Reduce lines of generated code in the designer by splitting it into multiple files
Allow multiple persons to work on different complex tabpages

Cons:

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.

However, User Controls should not be used for every form in the application. They should only be used for elements which are shared between several forms, such as search and address controls.



Figure: Bad use of user controls - all the forms in the application are user controls



Figure: Bad use of user controls - all of the controls on this form are on a user control, but are only used once



Figure: Good - user controls are only used for shared controls

Do you know how to design a user friendly search system?

A search system should be separate from the data entry fields (on a different form), to avoid confusion, should have variable criteria, and should have advanced options which allow the user to search any field at all.



Figure: Bad Search System (Controls are on the same form as the data entry controls)



Figure: Good Search System (see the SSW .NET Toolkit)

Do you use Validator controls?

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
Bad, 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
Good, 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.



Figure: .NET ErrorProvider Control with a custom balloon tooltip

SSW Extended Provider
The 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).



Figure: SSW Extended Provider controls and properties on a TextBox


Do you use DataSets or create your own business objects?

In .NET, there are two ways to pass data through the layers of your application. You can:

Use DataSet objects, OR
Write your own custom business objects

There are two very different opinions on this matter amongst .NET developers...
The PROs of the 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
DataSet
objects automatically 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.

The 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.

Microsoft's official word on this matter is explained in Designing Data Tier Components and Passing Data Through Tiers.

Do you use the designer for all visual elements?

The designer should be used for all GUI design. Controls should 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.



Figure: Bad - Connection and Command objects in the Designer



Figure: Good - only visual elements in the designer

Do you always use the Visual Studio designer for data binding where possible?

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.



Figure: Simple data binding (binding to a single property) in the designer



Figure: Complex data binding (binding to a list) in the designer
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);
}
}

Do you avoid using MDI forms?

MDI 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)

What about tabs, as used in VS .NET, and browsers such as Mozilla and CrazyBrowser? They are good for developers, but not users. Users are used to Outlook, which does not use MDIs at all. If the users want to group windows, Windows XP lets you "Group Similar Taskbar Icons".



Figure: Bad Example (Word 2003 in MDI mode)



Figure: Good Example (Word 2003 with Default Settings)

Me.IsMdiContainer = true;

ClientForm frm = new ClientForm();
frm.MdiParent = this;
frm.Show();
Bad code example using MDI forms

ClientForm frm = new ClientForm();
frm.Show();
Good code 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

Do you have a correctly structured common code assembly?

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?.

Do you include Exception Logging and Handling?

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 Block
Microsoft 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.



Figure: Exception Reporting Web Service

Log4Net is an open-source logging library for .NET base on the Log4J library. It is extremely capable, and will suit the logging needs of my current project, but I can't imagine a case where I wouldn't be able to find a place for this tool in future projects.
One of the standout attribute of this library is its documentation. Its use of XML code comments, and extensive reference documentation is a credit to the authors, and has set the standard of how we would like our developers to achieve in our code.

Log4Net allows you to pepper you code with log writing methods of different severities. You can then define in a separate XML .config file how reports on each severity error should be sent (in fact, even inside these severities you can decide how particular entries are reported). In our case "Info" log writes are sent to the Debug and Trace windows, Anything above "Error" is written to a SQL Server database, and "Critical" Errors are also emailed, and written to the Event Log. Because all of these settings are in a .config file, we can change it at runtime whilst the application is running on our production server (it has a file watcher to know when this config file has been edited).

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. The regular expressions use to check this are:

For C#:

To find a Main method without EMAB:
void/s+Main/([^)]*/)/s*{(?!/s*Application/.TreadException/s+/+=/s+new/s+(System/.Threading/.)?ThreadExceptionHandler/((Microsoft/.ApplicationBlocks/.ExceptionManagement/.)?ExceptionManager/.ThreadExceptionHandler/);).*/s+}

To find empty catch blocks:
catch/s*(?:/(/s*Exception/s+[A-Za-z][A-Za-z0-9-]*/s*/))?/s*{/s*}

For VB:

To find a main method without EMAB:
(<Reference/s+(Name/s+=/s+"SSW/.Framework/.ExceptionManagement")/s+(AssemblyName/s+=/s+"SSW/.Framework/.ExceptionManagement")/s+(HintPath/s+=/s+"[^"]*SSW/.Framework/.ExceptionManagement/.dll")/s+/>)

To find empty catch blocks:
Catch/s+(?:/w+?/s+?As/s+?[a-z]*Exception)?/s*End Try

Do you make a strongly-typed wrapper for app.config?

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.

Do you replace the standard .NET DataGrid?

The standard DataGrid in Visual Studio 2003 has some limitations. It is ugly compared to a ListView and does not support combo box or button columns, making it useless for many applications. (This is going to be fixed in Visual Studio 2005 with the new DataGridView control). However, Infragistics provide an XP-themed DataGrid which provides similar functionality to VS 2005's DataGridView.



Figure: Infragistics UltraGrid



Figure: Visual Studio 2005 DataGridView

Do you avoid 3rd party menus & toolbars?

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 (www.dotnetmagic.com)
Docking panels didn’t implement enough events and it is unclear what the events are doing
Menu control is OK
DotNetBar (www.devcomponents.com)

Instead of using the 3rd party controls, we are going to use the standard menu and toolbar until Visual Studio 2005 which provides Office 2003 style menus and toolbars with the new ToolStrip control, which it will be easy to upgrade to. Upgrading from these 3rd party controls will be difficult.



Figure: Visual Studio 2005 Office 2003-like controls (ToolStrip designer)[/b]

However, it would be better if VS 2005 stored the details of menus and toolbars in an XML file.

Do your Windows Forms applications support URLs?

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.



Figure: Outlook can automatically jump to a specified folder or item from a command line




Figure: Outlook address bar (Web toolbar) shows you the URL for every folder


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

HKEY_CLASSES_ROOT/AppName/URL Protocol = ""
HKEY_CLASSES_ROOT/AppName/Default Value = "URL:Outlook Folders"
HKEY_CLASSES_ROOT/AppName/shell/Default Value = "open"
HKEY_CLASSES_ROOT/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());
}

}[/code]
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

Do you include back & undo buttons on every form?

Following on from including a URL, every form should have a back and an undo button which takes you back to the previous screen, or reverses you last action respectively. This is just like Outlook has a back button to take you to the previous folder on the Web toolbar (see above).

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 Clipboard Return to Remember n/a Save Record Remember old values Execute procCustomerSave Close Form Return to Old values Reopen form

MenuActionUndoBack
CutRemember: Remember Text and Cursor Position
Cut To Clipboard
Return to Remembern/a
Save RecordRemember old values
Execute procCustomerSave
Close Form
Return to Old valuesReopen form
Sample code to come in the SSW .NET Toolkit.

Do you use NUnit to write Unit Tests?

Unit tests are a valuable tool when maintaining code, particularly in a team environment where you may have to fix a bug in someone else's code. Unit Tests ensure that you do not introduce new bugs in someone else's code, or code that you have not looked at for a while. We like NUnit because it is free, we have found that it is easy for developers to learn and it integrates well with Visual Studio. Visual Studio .NET 2005 integrates Unit Testing with Visual Studio Team System. We will use this when Visual Studio 2005 is released.

Unit tests should also be accessible from the Help menu to assist in troubleshooting when your users call up tech support. For more information see Rules to Better Interfaces.

Note: Unit testing also works with Web projects.

Are you Data Access Layers compatible with Web Services?

Data Access Layers should support not only direct connections to SQL Server (best performance when running locally) but also connections through web services (best performance when running remotely). There are three 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)

Do you use XP button for opening a web page taking action?

If it's simply opening a web page containing just more information or help then use hyperlink.



Figure: Simple hyperlink

But when it requires some form of action (e.g. generating reports, passing and processing values), use XP button with an image.

Figure: XP button with image (good)



Figure: Hyperlink (bad) Figure: Hyperlink on a button (bad) Figure: Normal button (bad)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐