Random Dev Notes

September 28, 2009

VFP MSBuild Task

Filed under: Development — Tags: — Tom Brothers @ 1:38 am

I first learned about Continuous Integration Servers about a year ago when I read a magazine article on the topic.  After reading the article, I decided that there was no way I was willing to continue with manual production builds anymore.  So I started to put together a CI Server.  In doing so, I was pleasantly surprised to see that almost everything needed to complete the new CI Server was readily available with a quick internet search.  However, there was one piece of the puzzle that was missing.  I was unable to find a resource that could produce a Visual FoxPro build for Cruise Control.Net.  Needless to say, I wasn’t about to let this minor setback deter me from my goal having a CI Server.  After reviewing my options, I decided to create a custom MSBuild Task to build my Visual FoxPro Applications.


Creating the VFP MSBuild Task

I started off by creating a C# Class Library Project named VfpBuildTask.  Then I added references for Microsoft.Build.Framework, Microsoft.Build.Utilities.v3.5, and Microsoft Visual FoxPro 9.0 Type Library.  With the basic setup completed, the project was ready for developing the custom MSBuild Task.

image image image

To create the custom MSBuild Task, I added a new class named VfpBuild.  The VfpBuild class inherits from the abstract class Microsoft.Build.Utilities.Task which includes an abstract method named Execute.  The Execute method will handle the building of Visual FoxPro Applications.  To start off with, I wanted to implement a C# version of the following VFP code:

oVfp = CREATEOBJECT("VisualFoxPro.Application")
oVfp.DoCmd("BUILD EXE <<MyApp>> FROM <<MyApp>>.pjx RECOMPILE")
oVfp.Quit()

Here is the C# code that I came up with for the VfpBuild class:

using System;
using System.IO;
using System.Threading;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
 
namespace VfpBuildTask {
    public class VfpBuild : Task {
        [Required]
        public ITaskItem[] Projects { get; set; }
 
        public string BuildType { get; set; }
 
        public override bool Execute() {
            VisualFoxpro.FoxApplicationClass vfp = null;
 
            try {
                // vfp = CREATEOBJECT("VisualFoxPro.Application")
                vfp = new VisualFoxpro.FoxApplicationClass();
 
                // loop through all the vfp projects 
                for (int index = 0, total = this.Projects.Length; index < total; index++) {
                    // get the project full path
                    string project = this.Projects[index].ItemSpec;
 
                    // create the build command
                    string buildCommand = string.Format("build {0} '{1}' from '{2}' recompile",
                                                            (string.IsNullOrEmpty(this.BuildType) ? "exe" : this.BuildType),
                                                            Path.GetFileNameWithoutExtension(project),
                                                            project);
 
                    // execute the build command
                    vfp.DoCmd(buildCommand);
                }
            }
            catch (Exception ex) {
                this.Log.LogError(ex.Message);
            }
            finally {
                if (vfp != null) {
                    vfp.Quit();
                }
            }
 
            return !this.Log.HasLoggedErrors;
        }
    }
}

At this point, I’m ready to test this class to see if it will actually create the build.  Now I needed to create and run a MS Build Project.  I created a very simple MS Build Project with the file name Build1.proj.

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Import Project="C:VfpBuildTaskMSBuildResourcesVfpBuildTask.Targets" />
 
    <Target Name="Build">
        <VfpBuild Projects="C:VfpBuildTaskAppsExeProgram1.pjx" />
    </Target>
</Project>

I ran this project from the command prompt using the following:

image

The VFP MSBuild Task built the application as expected.  Great!  But it also had a minor side effect which would prevent me from being able to add this to the CI Server Build Process.  It seems that something was preventing Visual FoxPro from exiting… resulting in a very common dialog:

image

After looking into this issue, I found that the Task Pane was causing this dialog.  I added code to close the Task Pane Window before calling the Quit method on the VFP instance.  To do this I just added a Thread.Sleep command to give the window some time to load and then I executed the VFP command “Clear All".

Here is the modified code:

using System;
using System.IO;
using System.Threading;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
 
namespace VfpBuildTask {
    public class VfpBuild : Task {
        [Required]
        public ITaskItem[] Projects { get; set; }
 
        public string BuildType { get; set; }
 
        public override bool Execute() {
            VisualFoxpro.FoxApplicationClass vfp = null;
 
            try {
                // vfp = CREATEOBJECT("VisualFoxPro.Application")
                vfp = new VisualFoxpro.FoxApplicationClass();
 
                // wait for the task pane window open
                Thread.Sleep(2000);
 
                // close the task pane window
                vfp.DoCmd("CLEAR ALL");
 
                // loop through all the vfp projects 
                for (int index = 0, total = this.Projects.Length; index < total; index++) {
                    // get the project full path
                    string project = this.Projects[index].ItemSpec;
 
                    // create the build command
                    string buildCommand = string.Format("build {0} '{1}' from '{2}' recompile",
                                                            (string.IsNullOrEmpty(this.BuildType) ? "exe" : this.BuildType),
                                                            Path.GetFileNameWithoutExtension(project),
                                                            project);
 
                    // execute the build command
                    vfp.DoCmd(buildCommand);
                }
            }
            catch (Exception ex) {
                this.Log.LogError(ex.Message);
            }
            finally {
                if (vfp != null) {
                    vfp.Quit();
                }
            }
 
            return !this.Log.HasLoggedErrors;
        }
    }
}

At this point, I’m technically done.  I can modify the CI Server build process to include building my Visual FoxPro Applications.  But now that I have the basics done… I can’t help but think of a few enhancements that I would like to include.  Plus, I’m considering the fact that I have not handled a potential build time dialog which my hang up the CI Server.  The dialog I’m referring to is the “Locate File” dialog.

image

Enhancing the VFP MSBuild Task -  Fix “Locate File” dialog issue:

The first change that I would like to make is preventing the “Locate File” dialog.  I can only think of two scenarios that would result in this dialog.  The first scenario is that the project is referencing a file that does not exist in the file system.  The second scenario is where the source code has a NewObject command which references a file that does not exist in the project or the file system.  These two scenarios are similar but require two different solutions.  In the first scenario, I can manually query the project file to see if any files are missing from the file system.  The second scenario I cannot completely solve but I can get close.  I can do this by querying the project file to generate a search path.  Setting the search path should allow the build process to find a file that hasn’t been added to the solution.  This will, of course, only work if the missing file is in a file location that has been referenced by another file in the project.

So how do I pull this off…?  First I added a reference LinqToVfp.dll, IQtoolkit.dll, and IQToolkit.Data.dll.  Then I create a class name VfpProjectItem to model the VFP Project table.  I only selected the few fields that I needed.

namespace VfpBuildTask {
    public class VfpProjectItem {
        public string Name { get; set; }
        public string Type { get; set; }
    }
}

Next I created a new method ProjectSetup and modified the Execute method to call this method.

public bool ProjectSetup(string project) {
         string connectionString = string.Format("Provider=VFPOLEDB.1;Data Source={0};", project);
         bool hasError = false;
         string projectFileName = null;
         List<string> fileList = null;
 
         using (VfpQueryProvider provider = VfpQueryProvider.Create(connectionString, null)) {
             provider.Connection.Open();
             projectFileName = Path.GetFileName(project);
 
             // Get a list of files with their relative path
             fileList = (from item in provider.GetTable<VfpProjectItem>(projectFileName)
                         // exclude the header item
                         where item.Type != "H"
                         select item.Name).ToList();
 
             provider.Connection.Close();
         }
 
         string homeDir = Path.GetDirectoryName(project);
         this.Log.LogMessage("Default Directory:  " + homeDir);
         this.vfp.DoCmd(string.Format("cd [{0}]", homeDir));
 
         // change the relative paths to absolute paths
         for (int index = 0, total = fileList.Count; index < total; index++) {
             string tempDir = homeDir;
             string itemPath = fileList[index];
 
             while (itemPath.StartsWith(@"..")) {
                 // go up a directory level
                 tempDir = Path.GetDirectoryName(tempDir);
                 // remove ..
                 itemPath = itemPath.Substring(3);
             }
 
             fileList[index] = Path.Combine(tempDir, itemPath);
         }
 
         // create a FileInfo object for each file
         List<FileInfo> fileInfoList = fileList.Select(file => new FileInfo(file)).ToList();
 
         // log an error message for each file that does not exist
         fileInfoList.Where(fi => !fi.Exists).ToList().ForEach(fi => {
             this.Log.LogError(string.Format("Cannot Locate File:  {0}", fi.FullName));
             hasError = true;
         });
 
         if (!hasError) {
             string searchPath = string.Empty;
 
             // create the search path
             fileInfoList.Select(fi => fi.DirectoryName.ToUpper()).Distinct().ToList().ForEach(dir => {
                 searchPath += dir + ";";
             });
 
             if (!string.IsNullOrEmpty(searchPath)) {
                 this.Log.LogMessage("Search Path:  " + searchPath);
                 this.vfp.DoCmd(string.Format("SET PATH TO [{0}]", searchPath));
             }
         }
 
         if (!hasError && !string.IsNullOrEmpty(this.Debug) && !string.IsNullOrEmpty(this.VersionNumber)) {
             string command = string.Format("MODIFY PROJECT '{0}'", project);
             this.Log.LogMessage(command);
             this.vfp.DoCmd(command);
 
             #region Debug
 
             if (!string.IsNullOrEmpty(this.Debug)) {
                 bool value = false;
 
                 if (bool.TryParse(this.Debug, out value)) {
                     command = string.Format("_vfp.ActiveProject.Debug = {0}", value ? ".t." : ".f.");
                     this.Log.LogMessage(command);
                     this.vfp.DoCmd(command);
                 }
                 else {
                     this.Log.LogError("Debug property is invalid:  {0}", this.Debug);
                     hasError = true;
                 }
             }
 
             #endregion
 
             #region VersionNumber
 
             if (!string.IsNullOrEmpty(this.VersionNumber)) {
                 // make sure auto increment is set to false if a version number was specified
                 command = string.Format("_vfp.ActiveProject.AutoIncrement = .f.");
                 this.Log.LogMessage(command);
                 this.vfp.DoCmd(command);
 
                 command = string.Format("_vfp.ActiveProject.VersionNumber = '{0}'", this.VersionNumber);
                 this.Log.LogMessage(command);
                 this.vfp.DoCmd(command);
             }
 
             #endregion
         }
 
         return !hasError;
     }

With this method in place, the NewObject scenario should compile just fine.  The missing project file scenario will no longer display the “Locate File” dialog.  Instead, this error will be logged to the console as seen in red below.

image

Enhancing the VFP MSBuild Task – Other minor enhancments:

Added OutputDir property, Debug property, and VersonNumber property.


Example MSBuild Project using the VFP MSBuild Task:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <MSBuildCommunityTasksPath>C:VfpBuildTaskMSBuildResources</MSBuildCommunityTasksPath>
    </PropertyGroup>
 
    <Import Project="$(MSBuildCommunityTasksPath)MSBuild.Community.Tasks.Targets"/>
    <Import Project="C:VfpBuildTaskMSBuildResourcesVfpBuildTask.Targets" />
    
    <Target Name="Build">
        <PropertyGroup>
            <GetVersionNumber>
                <![CDATA[
                    public static string ScriptMain() {
                        return DateTime.Now.ToString("yyyy.MMdd.hhmm");
                    }
                ]]>
            </GetVersionNumber>
        </PropertyGroup>
 
        <Script Language="C#" Code="$(GetVersionNumber)">
             <Output TaskParameter="ReturnValue" PropertyName="VersionNumber" />
  	</Script>
        
        <ItemGroup>
            <VfpExeProjects Include="C:VfpBuildTaskAppsExeProgram*.pjx" />
        </ItemGroup>
 
        <!-- build multiple Win32 executable build -->
        <VfpBuild Projects="@(VfpExeProjects)"
                  OutputDir="C:VfpBuildTaskDeploy"
                  Debug="False"
                  VersionNumber="$(VersionNumber)" />
 
        <!-- application build -->
        <VfpBuild Projects="C:VfpBuildTaskAppsAppProgram1.pjx"
                  OutputDir="C:VfpBuildTaskDeploy"
                  BuildType="App"
                  Debug="False"
                  VersionNumber="$(VersionNumber)" />
 
        <!-- multi-threaded com server build -->
        <VfpBuild Projects="C:VfpBuildTaskAppsDllPorgram1.pjx"
                  OutputDir="C:VfpBuildTaskDeploy"
                  BuildType="MTDLL"
                  Debug="False"
                  VersionNumber="$(VersionNumber)" />
    </Target>
</Project>

September 9, 2009

LINQ to VFP – Example #3

Filed under: Development — Tags: — Tom Brothers @ 11:14 am

For this example, I’ll modify the project created in LINQ to VFP – Example #2. I will add a new page that will use the Details View control. This new page will include the ability to insert a Product.

Page Setup: Add a new page Example3 to the project:

    • Example3.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Example3.aspx.cs" Inherits="Example3" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">
    <title></title>
</head>

<body>
    <form id="form1" runat="server">
        <div>
            <div style="color:Red;">
                <asp:Literal ID="ErrorMessage" runat="server" EnableViewState="false" />
            </div>
            <asp:DetailsView ID="ProductDetailsView"
                             runat="server"
                             DataSourceID="ProductDataSource"
                             DataKeyNames="ProductId"
                             AutoGenerateRows="False"
                             OnItemUpdated="ProductDetailsView_ItemUpdated"
                             OnItemDeleted="ProductDetailsView_ItemDeleted"
                             OnItemInserted="ProductDetailsView_ItemInserted">
                <Fields>
                    <asp:BoundField DataField="ProductID" HeaderText="Product Id" ReadOnly="True" />
                    <asp:BoundField DataField="ProductName"
                                    HeaderText="ProductName"
                                    SortExpression="ProductName" />
                    <asp:TemplateField HeaderText="Supplier" SortExpression="Supplier.CompanyName">
                        <ItemTemplate>
                            <%# Eval("Supplier.CompanyName")%>
                        </ItemTemplate>
                        <EditItemTemplate>
                            <asp:DropDownList ID="DropDownList1"
                                              DataSourceID="SupplierDataSource"
                                              DataValueField="SupplierId"
                                              DataTextField="CompanyName"
                                              SelectedValue='<%# Bind("SupplierId") %>'
                                              runat="server" />
                        </EditItemTemplate>
                    </asp:TemplateField>
                    <asp:TemplateField HeaderText="Category" SortExpression="Category.CategoryName">
                        <ItemTemplate>
                            <%# Eval("Category.CategoryName")%>
                        </ItemTemplate>
                        <EditItemTemplate>
                            <asp:DropDownList ID="DropDownList2"
                                              DataSourceID="CategoryDataSource"
                                              DataValueField="CategoryId"
                                              DataTextField="CategoryName"
                                              SelectedValue='<%# Bind("CategoryId") %>'
                                              runat="server" />
                        </EditItemTemplate>
                    </asp:TemplateField>
                    <asp:BoundField DataField="UnitPrice"
                                    HeaderText="UnitPrice" />
                    <asp:BoundField DataField="UnitsInStock"
                                    HeaderText="UnitsInStock" />
                    <asp:BoundField DataField="UnitsOnOrder"
                                    HeaderText="UnitsOnOrder" />
                    <asp:CheckBoxField DataField="Discontinued"
                                       HeaderText="Discontinued" />
                    <asp:CommandField ShowEditButton="True" />
                    <asp:CommandField ShowDeleteButton="True" />
                    <asp:CommandField ShowInsertButton="True" />
                </Fields>
            </asp:DetailsView>
            <iqw:DataSource ID="ProductDataSource"
                            runat="server"
                            ContextTypeName="WebExample.Model.Northwind"
                            TableName="Products"
                            RetrieveGeneratedId="True"
                            EnableDelete="true"
                            EnableInsert="true"
                            EnableUpdate="true"
                            OnInserted="ProductDataSource_Inserted">
            </iqw:DataSource>
            <iqw:DataSource ID="CategoryDataSource"
                            runat="server"
                            ContextTypeName="WebExample.Model.Northwind"
                            TableName="Categories" />
            <iqw:DataSource ID="SupplierDataSource"
                            runat="server"
                            ContextTypeName="WebExample.Model.Northwind"
                            TableName="Suppliers" />
        </div>
    </form>
</body>
</html>
    • Example3.cs
using System;
using System.Web.UI.WebControls;
using WebExample.Model;

public partial class Example3 : System.Web.UI.Page {
    protected void Page_Load(object sender, EventArgs e) {
        if (!this.IsPostBack) {
            this.ProductDetailsView.ChangeMode(DetailsViewMode.Insert);
        }
    }

    protected void ProductDataSource_Inserted(object sender, LinqDataSourceStatusEventArgs e) {
        Product p = e.Result as Product;
        this.ProductDataSource.Where = "ProductId = " + p.ProductID;
    }

    protected void ProductDetailsView_ItemUpdated(object sender, DetailsViewUpdatedEventArgs e) {
        if (e.Exception != null) {
            this.ErrorMessage.Text = e.Exception.Message;
            e.ExceptionHandled = true;
            e.KeepInEditMode = true;
        }
    }

    protected void ProductDetailsView_ItemInserted(object sender, DetailsViewInsertedEventArgs e) {
        if (e.Exception != null) {
            this.ErrorMessage.Text = e.Exception.Message;
            e.ExceptionHandled = true;
        }
    }

    protected void ProductDetailsView_ItemDeleted(object sender, DetailsViewDeletedEventArgs e) {
        if (e.Exception != null) {
            this.ErrorMessage.Text = e.Exception.Message;
            e.ExceptionHandled = true;
        }
    }
}

 

With the new page created, it is time to test the insert feature. Start by adding new Product information and then click the Insert link. At this point an exception has been thrown indicating that the “Field ProductId is read-only.” Now what does that mean? It means that I finally need to do a little explaining about how Mapping works in my examples.

Implicit Mapping: Up until this point I’ve been able to use Implicit Mapping. Implicit mapping allowed me to simple create data classes and let IQToolkit connect the classes to the FoxPro Tables.

Here are a couple key points about Implicit Mapping:

  • The Primary Key field must end with “ID” (upper case required).
  • Can handle singular and plural naming issues. Notice in the image below that the class name is singular and the table name is plural.
    image
  • Associations are determined by matching properties.
    image
  • Cannot determine if a Primary Key an auto generated value.

After my brief explanation of Implicit Mapping and with knowing about the Products table structure it should obvious why we cannot insert the new Product. The Implicit Mapping is trying to insert a value into the auto generated primary key field – ProductId. You can find the insert statement in the Output Window when in debug mode.

image

If you copy the insert statement and run it in VFP you will see that you get the same error.

image

So how do we fix this error…? It is time to stop using Implicit Mapping and start using a more explicit type of mapping. The IQToolkit includes two other type of mappings. I will use Attribute Mapping to finish up this example.


Three changes are required to setup the Attribute mapping.

  1. The IEntityTable<T> properties of the Northwind class need to be set as virtual.
    image
  2. Add a new class (NorthwindAttributes.cs) that includes all the attributes.
    image
  3. Modify the Northwind class to include the NorthwindAttribute class as the second parameter to the base constructor.
    image

After making these changes you should see that the insert is working as expected.

Blog at WordPress.com.