There are two good approaches for sharing UI code among .NET Compact Framework version 1.0 and 2.0–connected applications. One approach is to manually add controls to your forms, instead of using the form designer. You can still use the form designer in a temporary form to determine locations and sizes for particular controls. In your production code, however, you manually copy those settings and the controls into your source file.
The other approach is to separate business logic from UI code. Keep the business logic in shared source files, and keep different source files for your UI code. In that way, you can benefit from the additional properties and methods that are available for .NET Compact Framework 2.0–connected applications. In addition, you have the advantage of using partial classes to separate autogenerated code from your own code in that application. If you separate business logic from UI code, you can also make use of object-oriented techniques such as inheritance.
The following sections explore these two alternatives for sharing source code.
Using a Single Source Base
The next sample application has a single form that contains a DateTimePicker control, a Label control, and a TextBox control. The application is designed to select a date, to show it in the TextBox control, and store the selected date in the registry on the device. The application must work for the .NET Compact Framework versions 1.0 and 2.0, while sharing as much source code as possible. Because a DateTimePicker control is not available in the .NET Compact Framework 1.0, you must make use of OpenNETCF.org's Smart Device Framework 1.4 for the version of the application that works with the .NET Compact Framework 1.0. Since the Smart Device Framework 1.4 integrates with Visual Studio .NET 2003, the .NET Compact Framework 1.0 application is developed in Visual Studio .NET 2003. The .NET Compact Framework 2.0 version of the application is developed in Visual Studio 2005.
Figure 9 illustrates the UI of the application inside Visual Studio .NET 2003. In the ToolBox, you can see familiar .NET Compact Framework 1.0–based device controls and an additional set of OpenNETCF Controls. You can use these controls when you install the Smart Device Framework.
Figure 9. UI of a sample application to demonstrate sharing source code
The application uses a DateTimePicker control, which is not available in the .NET Compact Framework 1.0. However, by using the installed Smart Device Framework, you can use a DateTimePicker control in the .NET Compact Framework 1.0 application by dragging it from OpenNETCF Controls to the form. In doing so, you are making use of .NET Compact Framework 2.0 functionality inside a .NET Compact Framework 1.0 application.
In the following code example, you can see three empty event handlers, one for the Form1_Load event, one for the Form1_Closing event, and one for the dateTimePicker1_ValueChanged event. This code—running under the .NET Compact Framework 1.0—is the starting point for an application that uses two different approaches to share as much source code as possible between a version running under the .NET Compact Framework 1.0 and a version running under the .NET Compact Framework 2.0. The first approach to sharing code is to separate UI Code from Business Logic. The second approach to sharing code is to make use of Object Oriented Techniques. Both are described in the upcoming sections.
After you add a number of event handlers to the application, the source code for this form looks like the following code example (omitting the code that the form designer inserts).
using System;
using System.Drawing;
using System.Collections;
using System.Windows.Forms;
using System.Data;
namespace SmartDeviceApplication1
{
public class Form1 : System.Windows.Forms.Form
{
private OpenNETCF.Windows.Forms.DateTimePicker dateTimePicker1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.MainMenu mainMenu1;
public Form1()
{
InitializeComponent();
}
protected override void Dispose( bool disposing )
{
base.Dispose( disposing );
}
static void Main()
{
Application.Run(new Form1());
}
private void Form1_Load(object sender, System.EventArgs e)
{
}
private void Form1_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
}
private void dateTimePicker1_ValueChanged(object sender, System.EventArgs e)
{
}
}
}
Separating UI Code from Business Logic
You can share as much code as possible between .NET Compact Framework version 1.0 and 2.0 applications by separating business logic from UI code.
Like most applications, the sample application used in this article takes action upon receiving different events. The actions are executed in the different event handlers of the application. Apart from connecting event handlers to UI controls, you should try to implement any other code in one or more separate classes that provide the functionality for the application. To do so, you can create a new class in your .NET Compact Framework 1.0 project and create methods that will be called by the different event handlers inside Form1, as shown in the class diagram in the following figure.
Figure 10. Implementing business logic in a separate class
As you might expect, and as shown in the class diagram in Figure 10, the functionality to be executed in the application is put into the Form1Logic class. The only functionality for which Form1 is responsible is to maintain the UI and to call a method inside Form1Logic each time the user requests a particular action. The code inside Form1Logic can be shared between different versions of the application. As described earlier in this article, the functionality of the application is very simple:
-
In the Form1_Load event handler, the code will inspect the device registry to determine whether a selected date has been stored for the application. If it has, the program should retrieve that date, set it to the DateTimePicker control, and set it to the TextBox control. If no information is found in the registry, the current date will appear in both controls. The functionality to retrieve date information from the registry is implemented in the RetrieveLastDate method of the Form1Logic class.
-
In the Form1_Closing event handler, the code will store the date that is currently selected in the DateTimePicker control into the device registry. The functionality to achieve this action is implemented in the StoreLastDate method of the Form1Logic class.
-
In the dateTimePicker1_ValueChanged event handler, the code will copy the newly selected date to the TextBox control. Because no additional functionality is needed—this action is an update of a UI control—you can decide to leave that functionality inside Form1.
Inside Form1, you need to provide access to Form1Logic. To achieve this access, you can declare a variable of type Form1Logic in Form1 and instantiate it in the constructor of Form1, as you can see in the following code example.
private Form1Logic logic;
public Form1()
{
InitializeComponent();
logic = new Form1Logic();
}
Assuming that you have created the Form1Logic class, which contains all of the functionality that needs to be executed, you need to call the methods of Form1Logic only when events occur. The following code example shows the three different event handlers that are defined in Form1. Two of the event handlers are calling methods in Form1Logic, and one does a simple UI update.
private void Form1_Load(object sender, System.EventArgs e)
{
dateTimePicker1.Value = logic.RetrieveLastDate();
textBox1.Text = dateTimePicker1.Value.ToLongDateString();
}
private void Form1_Closing(object sender, <br>System.ComponentModel.CancelEventArgs e)
{
logic.StoreLastDate(dateTimePicker1.Value);
}
private void dateTimePicker1_ValueChanged(object sender, <br>System.EventArgs e)
{
textBox1.Text = dateTimePicker1.Value.ToLongDateString();
}
As you can see from the preceding code example, you have to maintain very little code inside Form1. This is an important point because Form1 will not be shared among the different versions of the application due to the form designer issues described earlier in this article. Omitting the functionality of Form1Logic for now, you need to create a new application in Visual Studio 2005 with the same UI as the previous application that will target the .NET Compact Framework 2.0. You need to add the same event handlers to get the same behavior for the application.
As you can see in Solution Explorer in Figure 11, the project already contains the class Form1Logic, which is implemented in the file Form1Logic.cs. Form1Logic is added as a link, as described earlier in this article. Therefore, the project has one physical source file—Form1Logic.cs—that will be shared between a .NET Compact Framework 1.0–connected application (CF1 application) that was developed in Visual Studio .NET 2003, and a .NET Compact Framework 2.0–connected application (CF2 application) that was developed in Visual Studio 2005.
Figure 11. UI of the sample application built in Visual Studio 2005
Similar to the code for the CF1 application, you can call methods of Form1Logic when events occur. The following code example shows the three different event handlers that are defined in Form1 for the CF2 application. Two of the event handlers are calling methods in Form1Logic; one does a simple UI update.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace DeviceApplication1V2
{
public partial class Form1 : Form
{
private Form1Logic logic;
public Form1()
{
InitializeComponent();
logic = new Form1Logic();
}
private void Form1_Load(object sender, EventArgs e)
{
dateTimePicker1.Value = logic.RetrieveLastDate();
textBox1.Text = dateTimePicker1.Value.ToLongDateString();
}
private void Form1_Closing(object sender, CancelEventArgs e)
{
logic.StoreLastDate(dateTimePicker1.Value);
}
private void dateTimePicker1_ValueChanged(object sender, <br>EventArgs e)
{
textBox1.Text = dateTimePicker1.Value.ToLongDateString();
}
}
}
As you can see, the code for the event handlers in the V2 application is identical to the code for the event handlers in the V1 application. The most important difference is that you now use the DateTimePicker control that is available in the .NET Compact Framework 2.0 instead of the DateTimePicker control from the OpenNETCF.org Smart Device Framework.
Because the DateTimePicker control in the Smart Device Framework uses the same properties and methods that are available for the .NET Compact Framework 2.0 DateTimePicker control, the code looks exactly the same.
The source code in Form1Logic.cs is most interesting because both applications share it.
using System;
using System.Windows.Forms;
#if NETCF1
using OpenNETCF.Win32;
#elif NETCF2
using Microsoft.Win32;
#endif
namespace BusinessLogic
{
/// <summary>
/// Summary description for Form1Logic.
/// </summary>
public class Form1Logic
{
private const string keyName = <br>"Software\\SmartDeviceApplication1";
private const string valueNameYear = "SelectedYear";
private const string valueNameMonth = "SelectedMonth";
private const string valueNameDay = "SelectedDay";
private const string valueNameHour = "SelectedHour";
private const string valueNameMinute = "SelectedMinute";
private const string valueNameSecond = "SelectedSecond";
public Form1Logic()
{
//
// TODO: Add constructor logic here
//
}
public DateTime RetrieveLastDate()
{
DateTime lastDate;
// Create / Open a registry key to retrieve the last selected<br> // date.
RegistryKey rk = Registry.LocalMachine.OpenSubKey(keyName);
if (rk == null)
{
// No value available in the created/opened subkey
lastDate = DateTime.Now;
}
else
{
int year = (int)rk.GetValue(valueNameYear);
int month = (int)rk.GetValue(valueNameMonth);
int day = (int)rk.GetValue(valueNameDay);
int hour = (int)rk.GetValue(valueNameHour);
int minute = (int)rk.GetValue(valueNameMinute);
int second = (int)rk.GetValue(valueNameSecond);
rk.Close();
lastDate = new DateTime(year, month, day, hour, minute,<br> second);
}
return lastDate;
}
public void StoreLastDate(DateTime lastDate)
{
// Create/open a registry key to retrieve last selected <br> // date.
RegistryKey rk = <br>Registry.LocalMachine.CreateSubKey(keyName);
if (rk == null)
{
MessageBox.Show("Could not create / open the registry <br>key");
}
else
{
rk.SetValue(valueNameYear, lastDate.Year);
rk.SetValue(valueNameMonth, lastDate.Month);
rk.SetValue(valueNameDay, lastDate.Day);
rk.SetValue(valueNameHour, lastDate.Hour);
rk.SetValue(valueNameMinute, lastDate.Minute);
rk.SetValue(valueNameSecond, lastDate.Second);
rk.Close();
}
}
}
}
In the preceding code example, conditional compilation enables us to distinguish between using directives to import types that are defined either in the "OpenNETCF.Win32" namespace or in the "Microsoft.Win32" namespace. Each application is designed to use the registry to store and retrieve data. The CF1 application does not have classes to access the registry. Therefore, in the CF1 application, you use registry classes that are available in the Smart Device Framework in the "OpenNETCF.Win32" namespace.
Because the .NET Compact Framework 2.0 has registry classes available, you can use those registry classes instead of the Smart Device Framework classes in the CF2 application. Again, the methods and properties of each set of registry classes are the same. You only need to refer to the correct namespace in order to use the particular registry classes that you require. The preceding code example is simple, and most of it can be shared between the two different versions of the application because the used SDF classes and .NET Compact Framework 2.0 classes closely match. You will probably need to do more work when working with a more complex application, but the approach can be the same.
When using a separate class for business logic, you must distinguish clearly between UI code and actual functionality. The UI code will be duplicated. In the UI code, you will have to call different methods that implement the business logic.
Using Object-Oriented Techniques
Another approach is to make use of object-oriented techniques like inheritance. In this approach, a class—the derived class—gains the nonprivate data and behavior of the base class in addition to other data or behaviors that it defines for itself. If you derive a class from Form1 and add business logic to that class, the derived class can also be a candidate for source code sharing between different versions of the application.
To demonstrate this approach, the functionality of the previous code example is implemented in a different way. The use of inheritance has advantages because you can make use of the polymorphism capabilities of the C# programming language in the following ways:
-
You can change the data and behavior of a base class.
-
You can replace the base member with a new derived member.
-
You can override a virtual base member.
When you ensure that the event handlers in the base class (Form1) are defined as protected virtual, they are candidates for overriding default behavior. You will make use of overriding functionality in the remainder of this article.
For this approach, as shown in the class diagram in Figure 12, the functionality to be executed in the application is again placed into the Form1Logic class, but Form1Logic inherits from Form1 and overrides the event handlers that were defined in the base class. Again, Form1 is responsible only for maintaining the UI. Because you now derive Form1Logic from Form1, however, there is no need to explicitly call member functions of Form1Logic.
Figure 12. Business logic in an inherited class
The UI for this sample application is exactly the same as the UI in the preceding code example as shown in Figure 11. In the following code example for a CF1 application, both the Form1_Load and Form1_Closing event handlers are declared as protected virtual, meaning that they can be overridden in a derived class. These event handlers contain no code. The actual implementation is in the derived class Form1Logic, that you can find later in this article. Even though the dateTimePicker1_ValueChanged event handler is declared as protected virtual as well, it does contain code to update the Text property of a text box. This means that a derived class may choose to override this functionality, but if it does not do so, the code in the base class will be executed.
The code example that follows also reveals a remarkable characteristic. Instead of instantiating a new Form1 class in the Application.Run method, you now instantiate the derived class Form1Logic. Because Form1Logic is derived from Form1, Form1Logic is in fact a form. Therefore, it can be passed to the Application.Run method.
static void Main()
{
Application.Run(new Form1Logic());
}
protected virtual void Form1_Load(object sender, System.EventArgs e)
{
}
protected virtual void Form1_Closing(object sender, <br>System.ComponentModel.CancelEventArgs e)
{
}
Protected virtual void dateTimePicker1_ValueChanged(object sender, <br>System.EventArgs e)
{
textBox1.Text = dateTimePicker1.Value.ToLongDateString();
}
To provide access to the different UI controls in the derived class, you also have to ensure that the Modifiers property of the UI controls to which you need access (in the sample, dateTimePicker1 and textBox1) are changed from private to protected.
Again, you need to create a separate project for a CF2 application with the same UI as the CF1 application, this time targeting the .NET Compact Framework 2.0. You need to add the same event handlers to get the same behavior in the application. To use functionality from the derived class Form1Logic while keeping it as a single source file for both the CF1 and CF2 applications, you need to add it to your project as a linked file, as described in the Create Different Versions of the Same Application section earlier in the article.
The following code example for the Form1 class of the CF2 application is similar to the code of the CF1 application shown previously.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace DeviceApplication2V2
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
protected virtual void Form1_Load(object sender, EventArgs e)
{
}
protected virtual void Form1_Closing(object sender, <br>CancelEventArgs e)
{
}
protected virtual void dateTimePicker1_ValueChanged(object <br>sender, EventArgs e)
{
textBox1.Text = dateTimePicker1.Value.ToLongDateString();
}
}
}
In .NET Compact Framework 2.0 applications, the Main method is moved to its own source file: Program.cs. Instead of instantiating a new Form1 class in the Application.Run method, you again must instantiate the derived class Form1Logic. With that change, the Program.cs file will look like the following code example.
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace DeviceApplication2V2
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[MTAThread]
static void Main()
{
Application.Run(new Form1Logic());
}
}
}
When you are using inheritance to share code between two different versions of your application, you should be aware of the following issues. Because you derive Form1Logic from Form1, Form1Logic is in fact a form. Inside both versions of Visual Studio, Form1Logic appears as a form in Solution Explorer. It even seems as though you can open Form1Logic in the form designer. In Visual Studio .NET 2003, you get an error when you try to open Form1Logic in the form designer, because the .NET Compact Framework 1.0 does not support visual inheritance. However, in Visual Studio 2005, you can open Form1Logic in the form designer. In addition, it may seem as though you can add more controls to your derived form. Doing so, however, will lead to problems.
Because you added Form1Logic as a link to the V2 project, the actual Form1Logic source file is located in a different folder than the other files of the V2 application. When you open Form1Logic with the form designer, Visual Studio 2005 attempts to open and create a Form1Logic.resx file. In doing so, it apparently detects that Form1Logic.cs is located somewhere else, which results in the error shown in Figure 13.
Figure 13. Error when you try to add controls to the derived form by using the form designer
If you want to add additional controls to your V2 application, you should add them to the base class (Form1). This advice is not a limitation because the base class is unique for the V2 application.
Again, the source code is most interesting in Form1Logic.cs because that code is shared between both applications. The following code example for Form1Logic.cs illustrates that Form1Logic is now derived from Form1. Again, you use conditional compilation to distinguish between V1 and V2 application logic. It is now also evident that one method is exclusively available for the V2 application—dateTimePicker1_ValueChanged.
using System;
using System.Windows.Forms;
#if NETCF1
using OpenNETCF.Win32;
#elif NETCF2
using Microsoft.Win32;
#endif
#if NETCF1
namespace SmartDeviceApplication2
#elif NETCF2
namespace DeviceApplication2V2
#endif
{
/// <summary>
/// Summary description for Form1Logic.
/// </summary>
public class Form1Logic : Form1
{
private const string keyName = <br>"Software\\SmartDeviceApplication2";
private const string valueNameYear = "SelectedYear";
private const string valueNameMonth = "SelectedMonth";
private const string valueNameDay = "SelectedDay";
private const string valueNameHour = "SelectedHour";
private const string valueNameMinute = "SelectedMinute";
private const string valueNameSecond = "SelectedSecond";
public Form1Logic()
{
//
// TODO: Add constructor logic here
//
}
protected override void Form1_Load(object sender, EventArgs e)
{
// Create/open a registry key to retrieve last selected <br> // date.
RegistryKey rk = Registry.LocalMachine.OpenSubKey(keyName);
if (rk == null)
{
// No value available in the created/opened subkey
dateTimePicker1.Value = DateTime.Now;
}
else
{
int year = (int)rk.GetValue(valueNameYear);
int month = (int)rk.GetValue(valueNameMonth);
int day = (int)rk.GetValue(valueNameDay);
int hour = (int)rk.GetValue(valueNameHour);
int minute = (int)rk.GetValue(valueNameMinute);
int second = (int)rk.GetValue(valueNameSecond);
rk.Close();
dateTimePicker1.Value = new DateTime(year, month, day, <br>hour, minute, second);
}
textBox1.Text = dateTimePicker1.Value.ToLongDateString();
}
protected override void Form1_Closing(object sender, <br>System.ComponentModel.CancelEventArgs e)
{
// Create/open a registry key to retrieve last selected <br>date.
RegistryKey rk = <br>Registry.LocalMachine.CreateSubKey(keyName);
if (rk == null)
{
MessageBox.Show("Could not create / open the registry <br>key");
}
else
{
rk.SetValue(valueNameYear, dateTimePicker1.Value.Year);
rk.SetValue(valueNameMonth, <br>dateTimePicker1.Value.Month);
rk.SetValue(valueNameDay, dateTimePicker1.Value.Day);
rk.SetValue(valueNameHour, dateTimePicker1.Value.Hour);
rk.SetValue(valueNameMinute, <br>dateTimePicker1.Value.Minute);
rk.SetValue(valueNameSecond, AUTHOR: See CD48.dateTimePicker1.Value.Second);
rk.Close();
}
}
#if NETCF2
protected override void dateTimePicker1_ValueChanged(object <br>sender, EventArgs e)
{
textBox1.Text = dateTimePicker1.Value.ToShortDateString();
}
#endif
}
}
Just as the sample that uses a separate class to implement the business logic, deriving a class from Form1 results in reasonably simple code. Again, most of the code can be shared between the two different versions of the application. This capability is due to the fact that the used Smart Device Framework classes and .NET Compact Framework 2.0 classes closely match.
This way of sharing source code has an advantage over the previously described way because less application-specific code needs to be written in the Form1 class. However, the application may experience a performance penalty because overwriting methods in a derived class results in an indirect call to the particular method. In fact, two separate calls are needed to reach the desired method. When you use inheritance, you must be careful not to open the derived classes in the form designer because you will encounter problems, as shown in figure 13. Also, because functionality is now found in a class derived from a Windows.Form class, the distinction between UI code and business logic is less clear.
Running each version of the application results in a slight difference in behavior, because the V2 application overrides an additional method to show a selected date in a short format in the textbox. Figure 14 shows both versions of the application, running on different devices with different versions of the .NET Compact Framework. The emulator on the left side of Figure 14 shows the .NET Compact Framework 1.0 version of the application running on a Pocket PC 2002 emulator. This version makes use of OpenNETCF.org’s Smart Device Framework. On the right side is the .NET Compact Framework 2.0 version of the application running on a Windows Mobile 5.0 Pocket PC emulator. As you can see, the user experience for both versions of the application is the same.
Figure 14. CF1 and CF2 application sharing the same source code