When Visual FoxPro developers take the plunge to learn .NET, the most common reaction is, “I could do such-and-such, this-and-that in VFP-how can I do it in .NET?”
This special edition of The Baker’s Dozen will offer solutions for many of the typical challenges that VFP developers face when tackling .NET. I’ll start by covering .NET solution and project structures and an overview of the .NET Framework, and I’ll spend time showing how to use .NET reflection to do some of the things that VFP developers could accomplish with macro-expansion. Then I’ll cover different .NET features such as Generics, ASP.NET 2.0, and I’ll show how to create a reusable data access component. Finally, I’ll build the architecture for a set of reusable data maintenance classes in .NET.
Beginning with the End in Mind
I have the same goal in writing this article that I have in my Baker’s Dozen articles in CoDe Magazine: I’ll write a set of how-to tips that I would love to have read when I set out to learn a new technology or feature. I started developing software in 1987 and can honestly say that the transition from VFP to .NET was the most challenging (and also rewarding) endeavor of my career.
There is no one simple answer to the question of a .NET equivalent to VFP’s macro-but fortunately, the .NET Framework has a namespace called System.Reflection that provides developers with functions for run-time discovery.
In this article, I’ll cover the following:
- Understanding .NET projects, solutions, and assemblies, and a quick language primer
- A quick tour through the common .NET Framework classes
- How to use reflection in place of the VFP macro
- Building a .NET data access class
- .NET Generics and anonymous methods in C#
- Some powerful features in ASP.NET 2.0 and AJAX
- The Baker’s Dozen Spotlight: building a reusable data maintenance form class (this covers the next four tips)
- Subclassing Windows Forms controls and implementing reusable data binding
- Building a data maintenance criteria container (UserControl)
- Building a data maintenance results container (UserControl)
- Building a data maintenance entry/edit container (UserControl)
- Working with data in DataSets using ADO.NET
At the end of this article, I’ll include a list of books, articles, and other resources that I recommend for further research.
Watching My Language
I wrote the example code in this article in C#. I prefer C# because I previously wrote C and C++. In most instances, developers can take the code in this article and port it to Visual Basic. However, Tip 5 contains code that uses anonymous methods, a new C# language feature with that doesn’t have a Visual Basic equivalent though it will be available in the next version. For that situation I’ll provide a workaround that works today.
Less Talk, More Code
I set a goal for this article to be short on talk and long on code. When I began the .NET learning curve, I got the most value out of books and articles that contained meaningful code samples. So for each tip, as much as possible, I’ll minimize the yakking and focus on the hacking. (There was a time when hacker meant something complimentary!)
Tip 1: Understanding .NET Solutions, Projects, Assemblies, and References
Let’s start by looking at the solution and project structure in Visual Studio 2005 for an actual .NET application. Figure 1 shows the complete solution for a demo .NET database application. A .NET solution is a collection of .NET projects. The .NET solution in Figure 1 consists of the following:
![Figure 1: Solution structure.](https://codemag.com/Article/Image/0703092/Figure 1 - sample solution.tif)
- Two solution folders, one for a framework of reusable classes (Common Ground Framework), and one for an actual demo application (Construction Demo).
- Subfolders to further categorize projects within a main folder.
- Under each solution you’ll see a series of .NET projects. Each project contains one or more related class files that compile to a single separate DLL. For instance, the project CGS.DataAccess contains a class file for basic data access functionality. The project compiles to CGS.DataAccess.DLL and can be used from other .NET projects.
- In addition, the project ConstructionDemo.Client.Winforms (in the folder structure Construction Demo…Client…Winforms) appears in bold, because I’ve defined it as the startup project. When I build this project, Visual Studio 2005 will create an executable file.
Many classes will refer to functions in base libraries, and will also inherit from previous classes in other .NET projects. In these situations, it is necessary to set add a reference to parent libraries. You can right-click on a project and select Add Reference from the short-cut menu (Figure 2).
![Figure 2: Solution project options.](https://codemag.com/Article/Image/0703092/Figure 2 - project options.tif)
In many instances you need to tell Visual Studio 2005 that some projects depend on others. You can also use the same short-cut menu (Figure 2) to establish project dependencies. For instance, project X may depend on projects A, B, and C. Figure 3 allows you to set the dependencies for any project in the solution.
![Figure 3: Project dependencies.](https://codemag.com/Article/Image/0703092/Figure 3 - project dependencies.tif)
Tip 2: A Whirlwind Tour through the common .NET Framework Classes
The .NET Framework has more classes than I have jazz CDs (trust me, I have many CDs). The power of .NET rests in the framework, with literally thousands of classes, categorized by namespaces. Let me walk you through five basic, common .NET classes.
One of the more interesting things I see is a single application with data maintenance forms (needlessly) built many different ways. VFP developers who remember back to the days of xBase code-generators like Genifer know the value of consistent data maintenance forms.
First, let me show you the DateTime class. The .NET Framework classes offer many different capabilities for date arithmetic. You can calculate the difference between two dates, determine future dates, and more. Look at these examples:
// Create two dates, Nov 1 2005 and Oct 1, 2005
DateTime dAgingDate = new DateTime(2005,11,1);
DateTime dInvDate = new DateTime(2005, 10, 1);
TimeSpan ts = dAgingDate.Subtract(dInvoiceDate);
int nDiffDays = ts.Days;
// Determine future dates
DateTime dNextDay = dtInvoiceDate.AddDays(1);
DateTime dNextMonth = dtInvoiceDate.AddMonths(1);
DateTime dNextYear = dtInvoiceDate.AddYears(1);
// Determine number of days in October 2002
int nDaysInMonth = DateTime.DaysInMonth(2002, 10);
// Is the year a leap year?
if(DateTime.IsLeapYear(2006)==true)
// Determine if a string represents a valid date
public bool IsValidDate(string cText)
{
bool lIsGoodDate = true;
// Convert to a date in a try/catch
try
{
DateTime dt = Convert.ToDateTime(cText);
}
catch (Exception)
{
lIsGoodDate = false;
}
return lIsGoodDate;
}
// Gets today (time will be midnight)
DateTime dToday = DateTime.Today;
// Get the current date/time
DateTime dNow = DateTime.Now;
On the Web you can find many examples of other date math capabilities. I recently discovered an excellent example available on the CodeProject Web site: codeproject.com/csharp/CSDateTimeLibrary.asp.
Now I’ll show you the String class, which contains the same types of functionality that VFP developers use for processing strings.
string cString = "KEVIN S. GOFF";
cString = cString.Insert(0, "MR. ");
// IndexOf returns 1st occurrence of a string
// within a string
int nPos = cString.IndexOf(".");
int nLen = cString.Length;
string cJustName1 =
cString.Substring(nPos+1, nLen-nPos-1).Trim();
string cJustName2 = cString.Replace("MR. ", "");
// LastIndexOf returns the last occurance
int nFinalPos = cJustName1.LastIndexOf(".");
Third, I’ll walk you through the Math class,
decimal nAmount = -219.1187M;
decimal nAbsAmount = Math.Abs(nAmount);
decimal nRound = Math.Round(nAbsAmount, 2);
decimal nMax = Math.Max(nAbsAmount, nAmount);
Fourth, let me illustrate the IO class, and some code for determining the current folder.
// DON'T DO THIS
string cDir =
System.IO.Directory.GetCurrentDirectory();
Unfortunately, the preceding code will not work if the application has changed the active folder, and is also not fully reliable in a distributed environment. To accurately determine the folder where the current assembly resides, use the following code:
// DO THIS INSTEAD
AppDomain currentDomain = AppDomain.CurrentDomain;
string cFolder =
currentDomain.BaseDirectory.ToString();
Finally, the .NET Framework contains a Timer class.
Timer myTimer = new Timer();
myTimer.Interval = 1000;
myTimer.Enabled = true;
myTimer.Tick +=
new System.EventHandler(MyTimerEvent);
public static void MyTimerEvent
(object source, EventArgs e)
{
// event code for timer event
}
Tip 3: How to Use .NET Reflection in Place of the VFP Macro
FoxPro developers commonly ask how to implement macro-expansion and the VFP EVAL function in .NET.
There is no one simple answer-but fortunately, the .NET Framework has an entire namespace called System.Reflection that provides developers with functions for run-time discovery. I’ll list some common scenarios and describe how you can address them using .NET reflection.
Sometimes developers need to execute a method or function where they don’t know the name of the method until run time. Listing 1 presents a reusable method called RunMethod, where developers can pass the name of the method to execute as a string, along with the assembly and class name where the method resides, as well as parameters for the method. RunMethod does the following:
- Loads the assembly using the .NET function LoadFrom into an assembly object
- Loops through all of the available .NET types in the assembly object using the .NET function GetTypes to find the specific class name that was supplied as a parameter.
- For the .NET type associated with the class name parameter, gets a reference to the method using GetMethod.
- Creates an instance of the class name type using CreateInstance and invokes the actual method you want to call using the .NET function InvokeMethod. If you specified any parameters for the method, they are passed as an object array.
Look at this example of how to use RunMethod:
// This example will run a method called
// MyFunction. It resides in MyAssembly.MyClass.
// I'll pass it two parameters…a string and an int
// It returns true or false
object[] args = new object[2];
args[1] = "MyParm1";
args[2] = 1;
object oReturn = RunMethod
("MyAssembly","MyClass","MyFunction",args);
// Need to cast the return value as the
// appropriate data type
bool lReturn = (bool)oReturn;
The code for RunMethod in Listing 1 may seem a bit daunting to those new to .NET. Fortunately you can write it as a reusable class and not have to worry about the complexities of it any longer. The code to call RunMethod in the snippet above isn’t much more code than what you’d have to write in VFP to construct a command syntax on the fly.
OK, enough about executing a method-how about discovering the available properties in a class? Listing 2 creates a test class with a handful of properties with different data types and a block of code that reads the class and loops through all the properties with the .NET function GetProperties, which reads information into a .NET object of type PropertyInfo. The PropertyInfo object contains the properties Name and PropertyType to determine each property’s name and datatype. The PropertyInfo object also contains a method called GetValue to determine the current value of the property.
How about determining if a particular property, event, or method actually exists? Listing 3 contains a partial counterpart of the VFP PEMSTATUS function. The code uses the .NET objects/methods PropertyInfo/GetProperty, EventInfo/GetEvent, and MethodInfo/GetMethod to determine if a particular PEM exists.
Finally, developers sometimes need to launch a form class without knowing until run time the name of the form class itself. Listing 4shows how to launch a .NET Windows form. Borrowing from some of the concepts in the previous listings you can use the Assembly.LoadFrom function to create an object reference to the assembly (DLL) containing the form, and then CreateInstance to create an instance of the form object. Note that CreateInstance returns a basic object, so you must cast the return value as a form object.
Tip 4: Building a Data Access Class
Another task that VFP developers will look to accomplish early on in .NET is building some kind of reusable .NET data access class. Listing 5 will get you started by creating a reusable function called ReadIntoDs that does the following:
- Calls a SQL Server stored procedure
- Optionally passes any stored procedure parameters
- Optionally sets a custom timeout if you have a specified stored procedure that will take a long time to execute
- Returns the results of the stored procedure as an ADO.NET dataset
We can use this function by creating a custom data access class (Listing 6) that inherits from the base class in Listing 5, and then calls our base function ReadIntoDs. A few things worth noting from Listing 6:
Subclassing the base .NET Windows Forms controls is different from VFP because developers must write code to do so. You cannot subclass visually, but the process will provide good exposure to the language and the .NET Framework.
Since the class inherits from the base data access class, you declare it this way:
using CGS.DataAccess;
..
public class daAgingReport : cgsDataAccess
You can load any SQL parameters into a collection and pass them to the base function ReadIntoDs, which will process them inside the method:
ArrayList aParms = new ArrayList();
oParms.Add(new SqlParameter(
"@cCustomerList", cClientList));
oParms.Add(new SqlParameter(
"@dAgingDate", dAsOfDate));
oParms.Add(new SqlParameter(
"@lShowDetails", lDetails));
Finally, since you’ve created a class that derives (inherits from) your base class, you can call the method directly.
dsResults =
this.RetrieveFromSP(cSPName,oParms,100);
// or call it without specifying a custom timeout
// dsResults =
this.RetrieveFromSP(cSPName, oParms);
Tip 5: Using .NET Generics
.NET generics is a new capability in Visual Studio 2005 and the .NET Framework 2.0. Generics warrants an entire book or set of articles, because the span of its coverage is so great. This tip will focus on two areas of generics: creating, filtering, and sorting type-safe collections, and writing methods that can handle multiple data types.
First, let’s look at type-safe collections. In Visual Studio .NET 2003, many developers used the ArrayList collection class to store a list of objects. In the code below, I am simply storing a list of integers: I could just as easily store a collection of form objects, datatables, or other objects. (And for that matter, in the previous tips, you used the ArrayList to store SQL parameter objects-I’ll come back to that in a minute).
ArrayList list = new ArrayList();
// implicit boxing, must convert value
// type to reference type
list.Add(3);
list.Add(4);
list.Add(5.0);
int nFirst = (int)list[0];
int total = 0;
foreach (int val in list)
total = total + val;
Using ArrayLists this way raises three issues:
- Regardless of what type I store in the ArrayList, the ArrayList stores it as an object. So .NET performs an implicit conversion, or “boxing” operation.
- When I retrieve a specific item from the ArrayList, .NET must convert the item from an object back to the source data type (and I must specify the data type). So .NET performs an “unboxing” operation.
- I have no compile-time type safety checking. In the ArrayList example I store two integers and then a decimal to the ArrayList. When I iterate through the collection on the last two lines, .NET will generate an error at run time because I am attempting to cast the decimal value (5.0) as an integer.
In Visual Studio 2005, I can use the List class from the System.Collection.Generic namespace. The List class addresses the issues in the ArrayList class by allowing me to define the specific type I wish to store for a specific instance of the class.
// Define the stored data type in the placeholder
List<int> aList = new List<int>();
aList.Add(3);
aList.Add(4);
aList.Add(5.0); // Generates a compile error
// nNo need for unboxing; value type stored in
// List<int > as an int, not an object
int nFirst = aList[0];
int total = 0;
foreach (int val in aList)
total = total + val;
Because I define the stored type when I create an instance of the List class, .NET does not need to perform any boxing or unboxing. .NET knows that I am storing an integer. Additionally, I do not need to cast a specific item in the List.
The power of the List collection class goes beyond the ability to store simple data types. You can store a collection of custom classes and then filter/sort them. In Listing 7 I create a simple custom class (CustomerRec) with four properties for customerID, locationID, CustomerName, and AmountDue. The listing manually creates a list of customer records as follows:
List<CustomerRec> oCustomerRecs =
new List<CustomerRec>();
oCustomerRecs.Add(new CustomerRec
(1, 1, "Customer 1", 200000));
oCustomerRecs.Add(new CustomerRec
(5, 2, "Customer 5", 100000));
After I populate the list (either manually, or from a DataSet or a DataReader), I can populate the list and then filter the list by using the FindAll method for the List class:
List<CustomerRec> oFilteredCustomers =
oCustomerRecs.FindAll(
(delegate(CustomerRec oRecParm)
{ return ((oRecParm.LocationID == 1 ||
oRecParm.LocationID == 2) &&
oRecParm.AmountDue > 15000); }));
Yes, the above code snippet is one line of code! The code demonstrates anonymous methods, a feature in C# that lets you encapsulate in-line code in a delegate. In the case of the code snippet above, the FindAll method expects a parameter that refers to a delegate for a Predicate method that returns a Boolean for whether each incoming record meets a condition I specify. Anonymous methods allow C# developers to place the code inside the delegate, thereby eliminating a new method.
You can also use anonymous methods to define your own custom sorting. Listing 7 also shows how to sort the filtered customer list by amount descending. In the snippet below I use the .NET function CompareTo and reverse the parameters (since I’m sorting by amount descending).
oFilteredCustomers.Sort
(delegate(CustomerRec oRec1, CustomerRec oRec2)
{ return
oRec2.AmountDue.CompareTo(oRec1.AmountDue);
});
You may be wondering, “What if I want to sort on location in ascending order, and amount descending within location?” You could still use the anonymous method approach, which would look like this:
oFilteredCustomers.Sort
(delegate(CustomerRec oRec1, CustomerRec oRec2)
// If the locations are the same,
// compare the second amount to the first.
return oRec1.LocationID == oRec2.LocationID ?
oRec2.AmountDue.CompareTo(oRec1.AmountDue) :
// If the locations are NOT the same
// compare the first location to the second.
oRec1.LocationID.CompareTo(oRec2.LocationID) ;
});
The snippet above uses the equivalent of the VFP IIF() function (pose a Boolean condition, then a question mark for the code to execute if the condition is true, then a colon for the code to execute if the condition is false).
Finally, you can bind a collection to a data-bound control (such as a DataGridView control):
this.dataGridView1.DataSource =
oFilteredCustomers;
Well, that’s enough about typed collections. Now let me introduce you to .NET’s generic methods. Sometimes you may have multiple blocks of code that differ only by the type of data. Prior to .NET generics developers had to either create function overloads, use reflection, or write multiple blocks of code.
Let’s consider some code that compares two values and returns the greater value of the two. I want to use it to compare strings, dates, integers, etc. Again, I could write multiple methods, or write one method with multiple overloads, or restore to some other trickery. However, .NET generics allow me to write a single method to cover multiple data types. Here is where .NET generics truly shine.
In the code below I’ll set up a method called CalcMax. Note how I use the letter T and the placeholder <T>. I don’t need to specifically use the letter T: I could use some other letter or an entirely different word. However T serves as a meaningful designator for a type placeholder.
public T CalcMax<T> ( T compVal1, T compVal2)
where T : IComparable
{
T returnValue = compVal2;
if (compVal2.CompareTo(compVal1) < 0)
returnValue = compVal1;
return returnValue;
}
In CalcMax I define a type placeholder for the return value and a type placeholder for the two parameters. The only rule is that the type passed must implement IComparable since I am using the CompareTo method of the parameters.
I can call CalcMax several times, specifying different data types each time.
double dMax = CalcMax<double>(111.11,333.23);
int intMax = CalcMax<int>(2, 3);
string cMax = CalcMax<string>("Kevin", "Steven");
DateTime dtMax = CalcMax<DateTime>
(DateTime.Today, DateTime.Today.AddDays(1));
Of course, the above code for CalcMax is pretty basic. Let’s go back and expand the data access class.
I use stored procedures and typed datasets heavily. One of my many “holy grail” quests has been to populate a typed dataset directly from a stored procedure.
For years I used a base method in my data access class to populate a plain vanilla dataset from a stored procedure with parameters. Afterwards I would merge the dataset into a typed dataset. This certainly worked but it meant additional code and an additional processing step. What I wanted to do was pass an instance of a typed dataset into the base method and have the base method serve as a factory-to pump out a populated typed dataset.
.NET generics allow me to create such a class and then use it (Listings 8 and 9). Follow these steps:
- Create an instance of a typed dataset, and an instance of the data access class (which appears in Listing 9)
- Create a typed List of SQL parameters for the stored procedure (instead of using an ArrayList)
- Call the data access class method (ReadIntoDs) passing an instance of the typed dataset, the name of the stored procedure, and the typed List of parameters for the stored procedure
- Create the data access method ReadIntoDs (Listing 9), and specify a typed placeholder for the first parameter and for the return value. Note the restriction that the parameter must be a dataset since code inside the method will use dataset-specific properties and methods.
public T RetrieveDataIntoTypedDs<T>
(T dsTypedDs, string cStoredProc,
List<SqlParameter> oParmList)
where T : DataSet
- Define the standard connection object, data adapter, etc.
- Elbow grease time! ADO.NET reads stored procedure result sets with names of Table, Table1, Table2, etc. When I designed the typed dataset I could have used more descriptive names (dtClient, dtDetails, etc.) Therefore, I need to map the names Table, Table1, etc., to the names in the typed dataset using the TableMappings command of the data adapter.
- Fill the dataset from the data adapter, and return it.
Tip 6: Cool Features in ASP.NET 2.0 and AJAX
ASP.NET 2.0 is a major upgrade over the original ASP.NET (and light years ahead of classic ASP). Of all the enhancements in ASP.NET 2.0, two features in ASP.NET 2.0 are sure to get VFP and Web developers excited: Master pages, the ObjectDataSource control. Additionally, the new Microsoft ASP.NET Ajax Framework aides developers in building a richer web-based UI.
Master Pages allow Web developers to create a base Web page with a common appearance, layout, and behavior. Developers can then create subsequent application Web pages based on the master page.
In the master page, you add placeholders called ContentPlaceHolders where the actual application pages will insert their custom content.
The new ObjectDataSource control allows developers to expose data-aware business objects/classes to data-bound controls such as the ASP.NET 2.0 GridView. I’ll freely admit that I’ve never been thrilled by controls that essentially pipe data from one area to another. While the ObjectDataSource is no different from any other such control in terms of placing restrictions, I’ve used it for almost a year now and am very happy with it.
In the July/August 2006 issue of CoDe Magazine I show a complete example of using the ObjectDataSource.
Last, but far from least, we have ASP.NET AJAX (formerly “Atlas”). AJAX is an exciting new technology that has many developers, myself included, giving stronger consideration to the possibility of building rich and responsive user interfaces in a browser.
Like .NET generics, AJAX has a wide-span-but many agree that the most important aspect of AJAX is the ability to create Web pages that implement partial updates without needing to write JavaScript. AJAX features a ScriptManager and UpdatePanel control that easily lets developers perform partial updates on certain areas of a page. Imagine being able to update a region of a page without doing a full postback and page refresh!
My Baker’s Dozen article from the September/October 2006 issue of CoDe Magazine contains a complete walkthrough of using the ObjectDataSource, in Tip 8. http://www.code-magazine.com/Article.aspx?quickid=0609041
Tip 7: The Baker’s Dozen Spotlight: Creating a Generic Data Maintenance Form with .NET Interfaces
As a contractor/consultant, I’m sometimes brought in to work on an existing system. One of the more interesting things I see is a single application with data maintenance forms built a dozen different ways. VFP developers who remember the days of xBase code-generators like Genifer know the value of consistent data maintenance forms.
In 1995, Yair Alan Griver (YAG) demonstrated client-server-style data maintenance forms as part of his famous VFP 3 CodeBook, with general techniques for lookup criteria/results/entry pages. Since YAG is one of my big heros, one of my goals the last few years was to build a reusable architecture for standard data maintenance forms in .NET.
Figures 4, 5, and 6 demonstrate a basic example of a three-tab .NET Windows Form for data maintenance. (As an aside, on more than one occasion, I’ve demonstrated these forms to people who thought they were VFP forms!) The next several tips will cover the architecture, which is part of my Common Ground Framework for Visual Studio 2005. The architecture uses an interface-based approach and does not rely on .NET reflection.
Let’s first identify all the components that make up this architecture.
First, in place of reflection, the architecture uses a total of five .NET interfaces-three interfaces for each of the three entry tab containers (ICriteriaContainer, IResultContainer, and IEntryContainer), an interface for the base data maintenance form itself (IDataMaintenanceForm), and an interface for the data-bound controls on the third tab container for data entry-editing (IBoundControl). Table 1 lists the interfaces and their properties/methods, and Listing 10 contains the code for the interfaces.
The architecture also uses several base classes that implement the corresponding interfaces above: a base container/user control, three containers for the three tab containers, a base maintenance form, and base data-bound controls. Table 2 lists these classes.
The next five tips (eight through twelve) will cover the code to create data-bound controls, the base maintenance form, and then the three tab containers for criteria/results/entry-edit.
Tip 8: Building a Base Data Maintenance Form
Initially, I included the complete source code listing for the base data maintenance form class. Then I realized it was much too long for an article, and those interested in it would be more likely to review it as part of the download project.
So download project contains a base data maintenance class called cgsFrmDataMaintenance.cs. A description for the most critical properties and methods is as follows, and then you can review the code as part of the download project.
- The page contains an enumeration called MaintenancePage, with three values for whether the user is on the Criteria, Results, or DataEntry page.
- The page contains object references to the three container pages in any implementation of the form (oCriteriaContainer, oResultContainer, and oEntryContainer).
- The GetContainerInterfaces method establishes which of the design-time containers should be associated with the three container object references above.
- The form mimics a pageframe with three command buttons that control which of the three container controls is active. Each of the three buttons (btnCriteria, btnResults, and btnData) act as the tabs, with method code to set the corresponding panel as active.
- The Click event of the three buttons sets the corresponding container as the current active container. The Click event also calls a method called SetCommandButtons, which determines which buttons on the toolbar (in the upper right corner) should be enabled/disabled.
- The virtual hook method SetCriteriaPage is called when the user clicks on the Criteria command button. This calls SetCommandButtons to set the toolbar accordingly. There are also virtual methods called SetResultPage and SetDataPage for the Results and Data pages.
- A binding manager property (bmgr) for binding on the results page, along with a DataSet property
- NET does not have a direct equivalent of the VFP WAIT WINDOW. However, you can simulate one by placing a label at the bottom of a base form class and then implement two base methods: SetMessageOn and SetMessageOff. Developers can call the former method anytime the application needs to display a status message (“Retrieving data…please wait”) and the latter method to turn the message off.
- The virtual hook method ExecuteCriteria, covered in Tips 10 and 11.
- The GetPrimaryKey method, which returns the primary key value from the current selected row on the results page. Also, the GetCurrentRow method, which returns the datarow from the full result set for the current selected row on the same results page.
Tip 9: Subclassing Windows Form Controls
Subclassing the base .NET Windows Forms controls is different from VFP because developers must write code to do so. You cannot subclass visually, but the process will provide good exposure to the language and the .NET Framework. Listing 11 contains the complete source code for a control library that also includes method code for two-way data binding. Take a look at these highlights of the class file, in no order of importance:
- The class contains subclassed controls for the CheckedListBox, Panel, Label, DateTimePicker, Button, MaskedTextBox, ComboBox, and CheckBox controls.
- The data-bound controls all implement the cgsInterfaces.IBoundControl interface. Remember that any class that implements IBoundControl must contain properties for a data source, as well as a BindControl method.
- The class in Listing 11 also contains a method called ShowData, for the cgsContainerEntry class-this method loops through all the data-bound controls on the page and calls the BindControl method for the control.
- When subclassing a Windows Forms control it is not enough to define a default font by simply using one line of code to set the font property in the control’s constructor. You must override the Font property and utilize the base keyword:
override public Font Font
{
get { return base.Font; }
set { base.Font = value; }
}
public cgsDateTime()
{
base.Font = new Font("Verdana",8);
}
- The subclassed MaskedTextBox control contains two methods (DecimalToCurrencyString and CurrencyStringToDecimal) for parsing and converting from string to currency (and back)
Tip 10: Building a Data Maintenance Criteria Screen
Figure 4 shows the criteria container of the maintenance form. You can build any type of criteria container you want by doing the following:
![Figure 4: The criteria tab of a general data maintenance form.](https://codemag.com/Article/Image/0703092/figure 4 - data maintenance criteria.tif)
using CGS.WinForms.Controls;
public class CtrEmployeeCriteria :
cgsContainerCriteria
Tip 11: Building a Data Maintenance Results Screen
Figure 5 shows the results container of the maintenance form. You can build any type of results container to show the results of the criteria from the first page, by doing the following:
![Figure 5: The results tab of a general data maintenance form.](https://codemag.com/Article/Image/0703092/figure 5 - data maintenance results.tif)
using CGS.WinForms.Controls;
public class CtrEmployeeResults :
cgsContainerResult
Tip 12: Building a Data Maintenance Data Entry/Edit Screen with Data Binding
Figure 6 shows the entry/edit container of the maintenance form, which, by default, loads the current selected row from the results container. You can build any type of edit container to show the complete data for the item selected on the second user control by doing the following:
![Figure 6: The entry/edit tab of a general data maintenance form.](https://codemag.com/Article/Image/0703092/figure 6 - data maintenance edit.tif)
using CGS.WinForms.Controls;
public class CtrEmployeeResults :
cgsContainerEntry
Tip 13: Data Handling with ADO.NET
One of the more controversial topics in the VFP world is the capabilities of ADO.NET. Many dismiss ADO.NET as weak because it does not have the same capabilities as the VFP cursor.
Yes, ADO.NET is not as powerful as the VFP cursor-but many Fox developers (and even some .NET developers) aren’t fully aware of the capabilities of ADO.NET. Here is a bullet point list of some of the main properties and functions available in ADO.NET.
A RowFilter property to filter on a DataTable, using English-like syntax
The ability to create a PrimaryKey property and then perform the equivalent of a VFP SEEK, using the Find method.
The ability to create data relations between DataTables in the same DataSet, based on one or more common columns.
Functions to aggregate columns using the SUM function.
A hierarchical object model of DataSet, DataTable, DataRow, and DataColumn objects.
I’ve written two detailed articles on the capabilities of ADO.NET that contain many code samples. So due to the length of this article, I’ll simply refer readers to the following prior Baker’s Dozen articles:
http://www.code-magazine.com/Article.aspx?quickid=0607061
http://www.code-magazine.com/Article.aspx?quickid=0601031
If You’re on Overload, Don’t Fret
Confused yet? Fret not-the download project from my Web site (www.commongroundsolutions.net) contains some additional documentation on the code from this article. Specifically, the documentation contains some step-by-step instructions for building data maintenance forms in Tips 7 through 12. If you still have questions, feel free to send me an e-mail me at kgoff@commongroundsolutions.net.
Recommended Reading
Throughout this article I recommended different articles or Web sites related to each section. As far as a general book on the topic, I strongly recommend Kevin McNeish’s book. .NET for VFP Developers.
If you want to dig deeper into reflection, Rick Strahl has written some outstanding articles on .NET reflection.
http://west-wind.com/weblog/posts/256.aspx
http://west-wind.com/presentations/DynamicCode/DynamicCode.htm
If you want to learn more about .NET generics, Tod Golding’s book Professional .NET Generics (Wrox Press) is an outstanding resource.
There are several excellent references for learning more about ASP.NET and ASP.NET AJAX:
- The official Microsoft ASP.NET AJAX site: http://ajax.asp.net/
- Rick Strahl, “Atlas Grows Up” http://www.code-magazine.com/Article.aspx?quickid=060073
- Marco Bellinaso, ASP.NET 2.0 Website Programming: Problem - Design - Solution (Wrox Press)
- Matthew MacDonald, Pro ASP.NET 2.0 in C# 2005 (Apress)
- Dino Esposito, Programming Microsoft ASP.NET 2.0 Applications: Advanced Topics (Microsoft Press)
At the risk of self-promotion, I’ve written a number of Baker’s Dozen articles in CoDe Magazine over the last two years on such topics as ADO.NET, Remoting, Transact-SQL, and even automating PowerPoint and Excel from within a .NET application.
Additionally, you may also want to check out my new book, Pro VS 2005 Reporting using SQL Server and Crystal Reports, from Apress. The book builds a complete reporting solution in a distributed environment, and contains chapters on stored procedures, Web services/remoting, data access, and report construction/generation.
A Final Suggestion
Simply stated, moving from VFP to .NET carries quite a learning curve. Some make it in a few months, others take longer. Some believe they’ve made it after a few months-and then realize a few months later how much they still have to learn.
For every function, .NET keyword, or language feature that I’ve mentioned, there are dozens and dozens of related topics that I can’t possibly cover in an article. Here’s where you have to invest some time. Take an afternoon or evening and use Google to search on information on .NET generics. Join Safari books online, if you or your employer can afford it, and start reading. You won’t get it all at once. Learning is a seminal process. But .NET is so deep and so rich that you almost have to “live” the platform.
I remember a common saying when moving from FoxPro 2.6 to VFP….“go back to school.” It’s ten years all over again. Plus ça change, plus c'est la même chose. (The more things change, the more they stay the same.)
Closing Thoughts
Have you ever submitted something (an article, a paper, some code, etc.) and thought of some good ideas AFTER the fact? Well, I’m the king of thinking of things afterwards. Fortunately, that’s the type of thing that makes blogs valuable. Check my blog (www.TheBakersDozen.net) for follow-up tips and notes on Baker’s Dozen articles…and maybe a few additional treats!
Listing 1: Using reflection to execute a method
using System;
using System.Reflection;
namespace ReflectionLibrary
{
public class ReflectionLibrary
{
public object RunMethod(string cAssembly,
string cClassName, string cFunctionName,
object[] args)
{
Assembly aAssembly =
Assembly.LoadFrom(cAssembly);
object oReturn = new object();
foreach(Type tType in aAssembly.GetTypes())
{
if(tType.Name.ToString().ToUpper() ==
cClassName.ToUpper())
{
MethodInfo MyMethodInfo =
tType.GetMethod(cFunctionName);
object oActivator =
Activator.CreateInstance(tType);
oReturn = MyMethodInfo.Invoke
(oActivator,args);
}
}
return oReturn;
}
}
}
Listing 2: Using reflection to determine the properties in a class.
public class MyTestClass
{
private int prop1 = 1;
private double prop2 = 2.0;
private string prop3 = "3";
public int Prop1
{
get { return prop1; }
set { prop1 = value; }
}
public double Prop2
{
get { return prop2; }
set { prop2 = value; }
}
public string Prop3
{
get { return prop3; }
set { prop3 = value; }
}
}
// Now some test code to evaluate the class
MyTestClass oMyTest = new MyTestClass();
oMyTest.Prop1 = 100;
oMyTest.Prop2 = 233.23;
oMyTest.Prop3 = "kevin";
Type t = typeof( MyTestClass );
PropertyInfo[] pia =
t.GetProperties(BindingFlags.Public |BindingFlags.Instance);
string cMessage = string.Empty;
foreach (PropertyInfo pi in pia)
cMessage += pi.Name + " " + pi.PropertyType + " " +
pi.GetValue(oMyTest, null) + "\n";
MessageBox.Show(cMessage);
Listing 3: Using reflection for PEMSTATUS
public bool PemStatus(object o, string name, string PEM)
{
bool lReturn = false;
switch (PEM.ToUpper())
{
case "P":
PropertyInfo pi = o.GetType().GetProperty(name);
lReturn = pi != null ;
break;
case "E":
EventInfo ei = o.GetType().GetEvent(name);
lReturn = ei != null;
break;
case "M":
MethodInfo mi = o.GetType().GetMethod(name);
lReturn = mi != null;
break;
default:
return false;
}
return lReturn;
}
// Test it out
string cMethod = "ProcessData";
if (this.PemStatus(oMyTestObject, cMethod, "M")) {
// if the method requires any parameters
// they need to be passed in as an array of objects.
object[] parms = new object[1];
parms[0] = "MyParm";
MethodInfo mi = oMyTestObject.GetType().GetMethod(cMethod);
string cMyResults = (string)mi.Invoke(oMyTestObject, parms);
}
Listing 4: Using reflection to launch a form and set properties
string DLLName = "MyAppForms.dll";
string ClassName = "MyAppForms.MyInvoiceForm";
Assembly oDLL;
object oClass;
oDLL = Assembly.LoadFrom(DLLName);
oClass = oDLL.CreateInstance(ClassName);
object [] args = new object[1];
args[0] = 111;
Form oForm = (Form)oDLL.CreateInstance(ClassName, true,
BindingFlags.CreateInstance, null, args, null, null);
// Code to set/get a property
oForm.GetType().InvokeMember
("MyNumericProp", BindingFlags.SetProperty, null,
oForm, new object[] { 1 });
int nValue = (int)oForm.GetType().InvokeMember
("MyNumericProp", BindingFlags.GetProperty, null,oForm, null);
oForm.Show();
Listing 5: A simple data access class
using System;
using System.Data;
using System.Data.SqlClient;
using System.ComponentModel;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace CGS.DataAccess
{
public class cgsDataAccess
{
// Pass the following:
// 1) An instance of a typed dataset you wish to fill
// 2) The name of the stored procedure
// 3) A List of SqlParameters
// 4) Optionally, a custom timeout
public DataSet ReadIntoDs(DataSet oDs, string cSP,
ArrayList oParmList)
{
return this.ReadIntoDs(oDs, cSP, oParmList, 0);
}
public DataSet ReadIntoDs(DataSet oDs, string cSP,
ArrayList oParmList, int nTimeOut )
{
// Get a connection object
SqlConnection oSqlConn = this.GetConnection();
// Create a data adapter, using the connection and SP name
SqlDataAdapter oSqlAdapt = new SqlDataAdapter(cSP, oSqlConn);
oSqlAdapt.SelectCommand.CommandType =
CommandType.StoredProcedure;
if(nTimeOut > 0)
oSqlAdapter.SelectCommand.CommandTimeout = nTimeOut;
// loop through the sqlparameter list, and apply the parms
foreach (SqlParameter oParm in oParmList)
oSqlAdapter.SelectCommand.Parameters.Add(oParm);
// this line actually executes the stored proc, and returns
// the results into the typed dataset
oSqlAdapter.Fill(oDs);
return oDs;
}
public SqlConnection GetConnection()
{
// New SqlConnection StringBuilder class
SqlConnectionStringBuilder oStringBuilder =
new SqlConnectionStringBuilder();
oStringBuilder.UserID = "UserID";
oStringBuilder.Password = "PassWord";
oStringBuilder.InitialCatalog = "InitialCatalog";
oStringBuilder.DataSource = "DataSource";
// check out Intellisense for other properties
return new SqlConnection(oStringBuilder.ConnectionString);
}
}
}
Listing 6: Using a data access class
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.SqlClient;
using System.Collections;
using CGS.DataAccess;
namespace ConstructionDemo.DataAccess
{
// Note that this inherits from the base data access class
// back in Listing 5
public class daAgingReport : cgsDataAccess
{
public DataSet GetAgingReportData(DateTime dAsOfDate,
bool lDetails, string cClientList)
DataSet dsResults = new DataSet();
string cSPName = "[dbo].[GetAgingReceivables]";
ArrayList aParms = new ArrayList();
oParms.Add(new SqlParameter("@cCustomerList", cClientList));
oParms.Add(new SqlParameter("@dAgingDate", dAsOfDate));
oParms.Add(new SqlParameter("@lShowDetails", lDetails));
dsResults = this.ReadIntoDS(cSPName,oParms,100);
// If you don't need a custom timeout,
// you can use the overload
// that only passes two parameters
// dsResults = this.RetrieveFromSP(cSPName, oParms);
return dsResults;
}
}
Listing 7: Using custom generic collections
public class CustomerRec
{
private int customerID;
public int CustomerID
{ get { return customerID; }}
private int locationID;
public int LocationID
{ get { return locationID; }}
private string customerName;
public string CustomerName
{
get { return customerName; }
}
private decimal amountDue;
public decimal AmountDue
{
get { return amountDue; }
}
public CustomerRec(int customerID, int locationID,
string customerName, decimal amountDue)
{
this.customerID = customerID;
this.locationID = locationID;
this.customerName = customerName;
this.amountDue = amountDue;
}
}
List<CustomerRec> BuildCustomers()
{
List<CustomerRec> oCustomerRecs = new List<CustomerRec>();
oCustomerRecs.Add(new CustomerRec(1, 1, "Customer 1", 200000));
oCustomerRecs.Add(new CustomerRec(2, 1, "Customer 2", 20000));
oCustomerRecs.Add(new CustomerRec(3, 1, "Customer 3", 11000));
oCustomerRecs.Add(new CustomerRec(4, 1, "Customer 4", 50000));
return oCustomerRecs;
}
private void TestCollection()
{
List<CustomerRec> oCustomerRecs = this.BuildCustomers();
List<CustomerRec> oFilteredCustomers = oCustomerRecs.FindAll(
(delegate(CustomerRec oRec)
{ return (oRec.LocationID == 1 || oRec.LocationID == 2)
&& oRec.AmountDue > 5000 ; }));
oFilteredCustomers.Sort
(delegate(CustomerRec oRec1, CustomerRec oRec2)
{ return oRec2.AmountDue.CompareTo(oRec1.AmountDue); });
this.dataGridView1.DataSource = oFilteredCustomers;
}
Listing 8: Another data access method, this time with .NET generics
public class cgsDataAccess
{
// Pass the following:
// 1) An instance of a typed dataset you wish to fill
// 2) The name of the stored procedure
// 3) A list of SqlParameters
// Function will execute the stored procedure,
// fill the typed dataset with the results, and then return it
public T RetrieveDataIntoTypedDs<T>
(T dsTypedDs, string cSP,
List<SqlParameter> oParmList) where T : DataSet
{
return this.RetrieveDataIntoTypedDs(dsTypedDs, cSP,
oParmList, 0);
}
public T RetrieveDataIntoTypedDs<T>
(T dsTypedDs, string cSP,
List<SqlParameter> oParmList, int nTimeOut)
where T : DataSet
{
// Get a connection object
SqlConnection oSqlConn = this.GetConnection();
// Create a data adapter using the connection and SP name
SqlDataAdapter oSqlAdapt = new SqlDataAdapter(cSP, oSqlConn);
oSqlAdapt.SelectCommand.CommandType =
CommandType.StoredProcedure;
if(nTimeOut > 0)
oSqlAdapter.SelectCommand.CommandTimeout = nTimeOut;
// loop through the sqlparameter list, and apply the parms
foreach (SqlParameter oParm in oParmList)
oSqlAdapter.SelectCommand.Parameters.Add(oParm);
// A little bit of "elbow grease…
// ADO.NET receives the resulting tablenames
// "Table", "Table1", etc.
// The typed dataset may have more descriptive names,
// such as dtOrderHdr, dtOrderDtl, etc.
// Therefore, you need to map in the table names accordingly
// So loop through the tables in the Typed DS, get the name,
// and map it to the corresponding table that ADO.NET would
// otherwise use as the default name
int nTableCtr = 0;
foreach (DataTable Dt in dsTypedDs.Tables) {
string cSource = "";
if (nTableCtr == 0)
cSource = "Table";
else
cSource = "Table" + nTableCtr.ToString().Trim();
// "Table" becomes whatever the actual name is
oSqlAdapter.TableMappings.Add(
cSource, Dt.TableName.ToString());
nTableCtr++;
}
// This line executes the stored proc, and returns
// the results into the typed dataset.
oSqlAdapter.Fill(dsTypedDs);
return dsTypedDs;
}
public SqlConnection GetConnection()
{
// New SqlConnection StringBuilder class
SqlConnectionStringBuilder oStringBuilder =
new SqlConnectionStringBuilder();
oStringBuilder.UserID = "UserID";
oStringBuilder.Password = "PassWord";
oStringBuilder.InitialCatalog = "InitialCatalog";
oStringBuilder.DataSource = "DataSource";
// Check out IntelliSense for other properties.
return new SqlConnection(oStringBuilder.ConnectionString);
}
}
Listing 9: Using the generic data access from Listing 8
// Create an instance of a typed dataset.
dsAgingReport odsAgingReport = new dsAgingReport();
// Create an instance of the data access method.
SimpleDataAccess oDataAccess = new SimpleDataAccess();
// Use the List class to create a collection of SQL parameters.
List<SqlParameter> oParms = new List<SqlParameter>();
oParms.Add(new SqlParameter("@dAgingDate", DateTime.Today));
oParms.Add(new SqlParameter("@lShowDetails", true));
// Call ReadIntoDs, passing the typed DS, name of the stored proc
// and the list of parameters.
odsAgingReport = oDataAccess.ReadIntoDs(odsAgingReport,
"[dbo].[GetAgingReceivables]",oParms);
Listing 10: Base data maintenance interfaces
// cgsInterfaces.cs
// .NET interfaces for IEntryContainer, IResultContainer, and
// ICriteriaContainer
// Also for IBoundControl and IDataMaintenanceForm
using System;
using System.Data;
using System.Windows.Forms;
namespace CGS.Interfaces
{
public class cgsInterfaces
{
public cgsInterfaces()
{
}
public interface IEntryContainer
{
void ShowData();
void SetPrimaryTable(DataTable dtTable);
void SetColumnsByName();
void SetEnabled(bool lEnabled);
void EditRecord();
void SaveData();
void AddRecord();
void ResetRecord();
}
public interface IResultContainer
{
BindingManagerBase ShowCriteriaResults();
int GetPrimaryKey();
DataRow GetCurrentRow();
}
public interface ICriteriaContainer
{
DataSet GetCriteria();
}
public interface IBoundControl
{
void BindControl();
void SetEnabled(bool lEnabled);
DataTable DtSource {get; set; }
DataColumn DcSource {get; set; }
bool lReadOnly {get; set; }
}
public interface IDataMaintenanceForm
{
BindingManagerBase bmgr {get; set; }
DataRow DrCurrentRow {get; set; }
void SetDataPage();
}
}
}
Listing 11: cgsControls.cs
// cgsControls.cs - subclassed version of common WinForm controls
// also contains code for generic data binding
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.Data;
using System.Globalization;
using CGS.Interfaces;
namespace CGS.WinForms.Controls
{
#region cgsCheckedListBox Class
public class cgsCheckedListBox : CheckedListBox
{
public cgsCheckedListBox()
{
this.Font = new Font("Verdana",8);
this.ThreeDCheckBoxes = true;
}
}
#endregion
#region cgsPanel Class
public class cgsPanel: Panel
{
public cgsPanel()
{
this.BorderStyle = BorderStyle.Fixed3D;
this.Anchor = ((AnchorStyles)((((
AnchorStyles.Top
| AnchorStyles.Bottom)
| AnchorStyles.Left)
| AnchorStyles.Right)));
}
}
#endregion
#region cgsLabel Class
public class cgsLabel: Label
{
override public Font Font
{ get { return base.Font; }
set { base.Font = value; } }
public cgsLabel()
{
base.Font = new Font("Verdana",8);
}
}
#endregion
#region cgsDateTime class
public class cgsDateTime : DateTimePicker,
cgsInterfaces.IBoundControl
{
private bool m_lReadOnly;
public bool lReadOnly
{ get {return m_lReadOnly ;}
set {m_lReadOnly = value; } }
private DataTable m_DtSource;
public DataTable DtSource
{ get {return m_DtSource ;}
set {m_DtSource = value; } }
private DataColumn m_DcSource;
public DataColumn DcSource
{ get {return m_DcSource ;}
set {m_DcSource = value; } }
override public Font Font
{ get { return base.Font; }
set { base.Font = value; } }
public cgsDateTime()
{
base.Font = new Font("Verdana",8);
this.Format = DateTimePickerFormat.Short;
this.Width = 92;
}
public void BindControl()
{
if(this.m_DcSource != null)
{
this.DataBindings.Clear();
this.DataBindings.Add("Text",
this.m_DtSource,
this.m_DcSource.ColumnName.ToString());
}
}
public void SetEnabled(bool lEnabled)
{
this.Enabled = lEnabled;
}
}
#endregion
#region cgsButton class
public class cgsButton: Button
{
override public Font Font
{ get { return base.Font; }
set { base.Font = value; } }
public cgsButton()
{
base.Font = new Font("Verdana",8);
base.BackColor = Color.White;
}
}
#endregion
#region cgsMaskedTextBox class
public class cgsMaskedTextBox : MaskedTextBox ,
cgsInterfaces.IBoundControl
{
public override Font Font
{ get {return base.Font; }
set {base.Font = value; } }
private bool m_lReadOnly;
public bool lReadOnly
{ get {return m_lReadOnly ;}
set {m_lReadOnly = value; } }
private DataTable m_DtSource;
public DataTable DtSource
{ get {return m_DtSource ;}
set {m_DtSource = value; } }
private DataColumn m_DcSource;
public DataColumn DcSource
{ get {return m_DcSource ;}
set {m_DcSource = value; } }
private bool m_lDecimal;
public bool lDecimal
{
get {return m_lDecimal ;}
set {m_lDecimal = value; }
}
public cgsMaskedTextBox()
{
base.Font = new Font("Verdana",8);
this.Enter += new EventHandler(OnEnter);
this.Leave += new EventHandler(OnLeave);
this.KeyDown += new
KeyEventHandler(OnKeyDown);
if(this.PasswordField==true)
this.PasswordChar = '*';
}
public void DecimalToCurrencyString(object sender,
ConvertEventArgs cevent)
{
if(cevent.DesiredType != typeof(string)) return;
cevent.Value =
((decimal) cevent.Value).ToString("F2");
}
public void CurrencyStringToDecimal(object sender,
ConvertEventArgs cevent)
{
if(cevent.DesiredType != typeof(decimal))
return;
cevent.Value =
Decimal.Parse(cevent.Value.ToString(),
NumberStyles.Currency, null);
}
public void SetEnabled(bool lEnabled)
{
if(this.lReadOnly==true)
this.ReadOnly = true;
else
this.ReadOnly = !lEnabled;
if(this.ReadOnly==true)
this.BackColor =
Color.FromArgb(192, 192, 255);
}
public void OnKeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
SendKeys.Send("{TAB}");
}
public void OnEnter(object sender,EventArgs e )
{
this.SelectAll();
if(this.ReadOnly==false)
this.BackColor = Color.Yellow;
}
public void OnLeave(object sender,EventArgs e )
{
if(this.ReadOnly==false)
this.BackColor = Color.White;
}
public void BindControl()
{
if(this.m_DcSource != null)
{
this.DataBindings.Clear();
Binding newBinding ;
newBinding = new Binding(
"Text", this.m_DtSource,
this.m_DcSource.ColumnName.ToString());
if(this.lDecimal==true)
{
this.TextAlign =
HorizontalAlignment.Right;
newBinding.Format += new
ConvertEventHandler(
this.DecimalToCurrencyString);
newBinding.Parse += new
ConvertEventHandler(
this.CurrencyStringToDecimal);
}
this.DataBindings.Add(newBinding);
}
}
}
#endregion
#region cgsComboBox class
public class cgsComboBox : ComboBox ,
cgsInterfaces.IBoundControl
{
private bool m_lReadOnly;
public bool lReadOnly
{ get {return m_lReadOnly ;}
set {m_lReadOnly = value; } }
// For most controls, you only need a data source table
// and data source column for data binding.
// However, for a combo box, you need additional
// properties for the foreign lookup table,
// the display member, and the value member.
// The BindControl method from the IboundControl
// interface will handle these properties.
private DataTable m_DtSource;
public DataTable DtSource
{ get {return m_DtSource ;}
set {m_DtSource = value; } }
private DataColumn m_DcSource;
public DataColumn DcSource
{ get {return m_DcSource ;}
set {m_DcSource = value; } }
private DataColumn m_DcValueMember;
public DataColumn DcValueMember
{ get {return m_DcValueMember ;}
set {m_DcValueMember = value; } }
private DataTable m_DtForeignTable;
public DataTable DtForeignTable
{ get {return m_DtForeignTable ;}
set {m_DtForeignTable = value; } }
private DataColumn m_DcDisplayMember;
public DataColumn DcDisplayMember
{ get {return m_DcDisplayMember ;}
set {m_DcDisplayMember = value; } }
override public Font Font
{ get { return base.Font; }
set { base.Font = value; } }
public cgsComboBox()
{
base.Font = new Font("Verdana",8);
this.DropDownStyle = ComboBoxStyle.DropDownList;
}
public void SetEnabled(bool lEnabled)
{
this.Enabled = lEnabled;
}
public void BindControl()
{
if(this.m_DcSource != null)
{
this.DataSource = this.m_DtForeignTable;
this.DisplayMember =
this.m_DcDisplayMember.ColumnName.ToString();
this.ValueMember =
this.m_DcValueMember.ColumnName.ToString();
this.DataBindings.Clear();
this.DataBindings.Add(
"SelectedValue",
this.m_DtSource,
this.m_DcSource.ColumnName.ToString());
}
}
}
#endregion
#region cgsCheckBox class
public class cgsCheckBox : CheckBox,
cgsInterfaces.IBoundControl
{
private bool m_lReadOnly;
public bool lReadOnly
{ get {return m_lReadOnly ;}
set {m_lReadOnly = value; } }
private DataTable m_DtSource;
public DataTable DtSource
{ get {return m_DtSource ;}
set {m_DtSource = value; } }
private DataColumn m_DcSource;
public DataColumn DcSource
{ get {return m_DcSource ;}
set {m_DcSource = value; } }
override public Font Font
{ get { return base.Font; }
set { base.Font = value; } }
public cgsCheckBox()
{
base.Font = new Font("Verdana",8);
this.Width = 88;
}
public void BindControl()
{
if(this.m_DcSource != null)
{
this.DataBindings.Clear();
this.DataBindings.Add("Checked",
this.m_DtSource,
this.m_DcSource.ColumnName.ToString());
}
}
public void SetEnabled(bool lEnabled)
{
this.Enabled = lEnabled;
}
}
#endregion
#region cgsContainer
public class cgsContainer : UserControl
{
public cgsContainer()
{
this.BackColor = Color.FromArgb(192, 192, 255);
this.Width = 740;
this.Height = 464;
}
}
#endregion
public class cgsContainerCriteria : cgsContainer,
cgsInterfaces.ICriteriaContainer
{
public cgsContainerCriteria()
{
}
public virtual DataSet GetCriteria()
{
return new DataSet();
}
}
public class cgsContainerEntry : cgsContainer,
cgsInterfaces.IEntryContainer
{
public cgsContainerEntry()
{
}
public virtual void ShowData()
{
foreach(Control oControl in this.Controls)
if(oControl is cgsInterfaces.IBoundControl)
{
((cgsInterfaces.IBoundControl)oControl).
BindControl();
oControl.Refresh();
}
}
public virtual void SetPrimaryTable(DataTable dtTable)
{
foreach(Control oControl in this.Controls)
if(oControl is cgsInterfaces.IBoundControl)
((cgsInterfaces.IBoundControl)oControl)
.DtSource = dtTable;
}
public virtual void SetColumnsByName()
{
foreach(Control oControl in this.Controls)
if(oControl is cgsInterfaces.IBoundControl)
{
string cName =
oControl.Name.ToString().Trim();
cName =
cName.Substring(3,cName.Length-3);
((cgsInterfaces.IBoundControl)oControl)
DcSource =
((cgsInterfaces.IBoundControl)oControl).
DtSource.Columns[cName];
}
}
public virtual void SetEnabled(bool lEnabled)
{
foreach(Control oControl in this.Controls)
if(oControl is cgsInterfaces.IBoundControl)
{
((cgsInterfaces.IBoundControl)oControl).
SetEnabled(lEnabled);
oControl.Refresh();
oControl.Update();
oControl.Focus();
}
this.Refresh();
this.Update();
}
public virtual void EditRecord()
{
this.SetEnabled(true);
}
public virtual void SaveData()
{
this.SetEnabled(false);
}
public virtual void AddRecord()
{
this.SetEnabled(true);
}
public virtual void ResetRecord()
{
this.ShowData();
this.SetEnabled(false);
}
}
public class cgsContainerResult : cgsContainer,
cgsInterfaces.IResultContainer
{
public cgsContainerResult()
{
}
public virtual BindingManagerBase ShowCriteriaResults()
{
return null;
}
public virtual int GetPrimaryKey()
{
return 0;
}
public virtual DataRow GetCurrentRow()
{
return null;
}
}
}
Table 1: Base interfaces for a common data maintenance form architecture.
Class | Property/Method |
---|---|
ICriteriaContainer | DataSet GetCriteria() |
IResultContainer | BindingManagerBase ShowCriteriaResults() |
int GetPrimaryKey() | |
DataRow GetCurrentRow() | |
IEntryContainer | void ShowData() |
void SetPrimaryTable(DataTable dtTable) | |
void SetColumnsByName() | |
void SetEnabled(bool lEnabled) | |
void EditRecord() | |
void SaveData() | |
void AddRecord() | |
void ResetRecord() | |
IDataMaintenanceForm | BindingManagerBase bmgr |
DataRow DrCurrentRow | |
void SetDataPage() | |
IBoundControl | void BindControl() |
void SetEnabled(bool lEnabled) | |
DataTable DtSource | |
DataColumn DcSource | |
bool lReadOnly |
Table 2: Base classes for a common data maintenance form architecture.
Class | Inherits from |
---|---|
cgsContainer | UserControl |
cgsContainerCriteria | cgsContainer, cgsInterfaces.ICriteriaContainer |
cgsContainerEntry | cgsContainer, cgsInterfaces.IEntryContainer |
cgsContainerResult | cgsContainer, cgsInterfaces.IResultContainer |
cgsFrmDataMaintenance | Form, cgsInterfaces.IDataMaintenanceForm |
cgsDateTime | DateTimePicker, cgsInterfaces.IBoundControl |
cgsMaskedTextBox | MaskedTextBox , cgsInterfaces.IBoundControl |
cgsComboBox | ComboBox , cgsInterfaces.IBoundControl |
cgsCheckBox | CheckBox, cgsInterfaces.IBoundControl |