Random Dev Notes

January 7, 2010

IQToolkitCodeGen (alpha version)

Filed under: Development — Tags: , , — Tom Brothers @ 11:40 am

When I first started using LINQ to VFP I didn’t mind manually writing Entity classes and mapping the data but after awhile I found it to become a tedious task.  So I decided to write a code generation application that would work with LINQ to VFP and the IQToolkit SQL Server Provider.  This application has really turned out to be a play thing for me so that I can get some hands on learning with WPF and Spark View Engine.  But before I’d allow myself to get to far into playing around I wanted to make sure I got the core functionality done.  At this point I have an alpha build available for anyone interested. 

image


Settings Overview:

image

As expected, you can use a full connection string to access VFP or SQL Server.  There are also a few unconventional connection options.  You can specify the full path to a dbc file or a specify a directory for free tables.  Both of these options will be used to create a connection string for VFP.  You can also provide a modified version of a SQL Server connection string that includes a pipe delimited list of databases (Example:  Data Source=.;Initial Catalog=Northwind|AspNet;Integrated Security=True). 

image
The Data Context Settings were designed with two different Data Contexts types in mind.  The first Data Context is an Entity Provider which is basically a class that has a property for each Entity.  This type of Data Context is what the IQToolkit Tests use.  The other type of Data Context is a based on a Repository Pattern which uses Generic methods instead of accessing an Entity property. 

The Data Context Settings allow you to specify the class name, output file, and namespace for both types of these Data Contexts.  The Base Class setting is for the Repository Data Context.

image
The Entity Settings allow you to specify the file extension, namespace, output path, and template.  There is only one Entity template available. 
 
image
The Mapping Settings can be used to create an Attribute Mapping class or an Xml Mapping file.


By default, the Entity Provider Template was designed to work with the Attribute Mapping Template and the Repository Provider Template was designed to work with the Xml Mapping Template.


Getting Started:

Enter all the setting values and then click the Load Data button.  Then the grids will be populated with the Tables, Columns, and Associations information.

The Tables grid is pretty simple to use.  Just check the checkbox in the Include column if you want an Entity class created.  The Entity class name can be specified using the grid’s Entity Name column.

image

The Columns grid shows the Columns of the selected row in the Tables grid.  This grid has a few more available options than the Table grid but is just as easy to use.  Check the checkbox in the Include column if you want the Entity class to include the property for the selected column.  In this grid, you can also change the name and type of the property as well as identify the primary key and indicate if it is an auto generated key.

image

The last grid shows the Associations.  This grid has an Include column and a Property Name column that can be set.

image

Click the Generate Files button after all the mapping information has been entered to complete the process.


Don’t like my templates?

The template files are distributed with this application so feel free to modify the templates as needed. Additionally, if you feel that you need to add a new template just add it to the appropriate folder and it will show up in the combo box after resetting the application.

This is a diagram shows the Template classes along with classes that they reference.

image

January 1, 2010

Using the Spark View Engine in a Desktop Application

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

I recently decided to write a code generation application using the Spark View Engine.  I started off by reviewing the provided Samples Solution and found the EmailOrTextTemplating Project.  This Project seemed to be a perfect example of what I needed.  Using the DefaultMessageBuilder class as a starting point, I created a new WinForm Application which included a very simple template named MyTemplate.spark.  This file contained one expression “${MyText}”.  After creating the template file, I added a reference to Spark.dll and then I modified the Program file with the following:

using System;
using System.IO;
using System.Windows.Forms;
using Spark;
 
namespace SparkViewEngineTest {
    static class Program {
        [STAThread]
        static void Main() {
            SparkViewEngine engine = new SparkViewEngine();
 
            var descriptor = new SparkViewDescriptor().AddTemplate("MyTemplate.spark");
 
            MyTemplate view = (MyTemplate)engine.CreateInstance(descriptor);
            view.MyText = "blah";
 
            StringWriter sw = new StringWriter();
            view.RenderView(sw);
 
            MessageBox.Show(sw.ToString());
        }
    }
 
    public abstract class MyTemplate : AbstractSparkView {
        public string MyText { get; set; }
    }
}

When I ran the program I got an error indicating that the template file didn’t exist.  The path provided in the exception showed that the template file was expected to be found in a “Views” subfolder.  I thought that this was some weird constraint so I moved my file into a “Views” subfolder.  After moving the file I re-ran the application.  Then I got a new exception complaining about “’SparkViewBase’ does not exist in the namespace ‘Spark’”.  At this point I was wondering what the heck I missed.  So I went back to the ExmailOrTextTemplating Project to see what I overlooked.  After a little while I noticed that the web.config has some Spark specific settings.  The one setting that caught my attention was PageBaseType.  So I went back to my code and set the PageBaseType setting to the MyTemplate class.  This change corrected the last error.  Perfect, now I have some working code (or so I thought).

using System;
using System.IO;
using System.Windows.Forms;
using Spark;
 
namespace SparkViewEngineTest {
    static class Program {
        [STAThread]
        static void Main() {
            SparkViewEngine engine = new SparkViewEngine();
            engine.Settings.PageBaseType = typeof(MyTemplate).FullName;
    var descriptor = new SparkViewDescriptor().AddTemplate("MyTemplate.spark");
 
            MyTemplate view = (MyTemplate)engine.CreateInstance(descriptor);
            view.MyText = "blah";
 
            StringWriter sw = new StringWriter();
            view.RenderView(sw);
 
            MessageBox.Show(sw.ToString());
        }
    }
 
    public abstract class MyTemplate : AbstractSparkView {
        public string MyText { get; set; }
    }
}

With working code in hand, I created a Test Project and started writing some tests for my code generation templates.  Everything worked as expected.  Great.  Then I moved on to writing the rest of the code generation application.  During testing of the code generation application I received an exception when rendering the templates.  As it turns out, all my template testing up until this point was against template files using a relative path.  But my application was using full paths.  Not testing this scenario was clearly an oversight on my part… but who would have thought that this mattered…?

Ok… back to my Spark View WinForm Test Application.  When I tested a full path I got this exception.

image

Dang… using the Spark View Engine turned out to be a little more challenging than expected.  And what the heck is that exception all about…?  Alright, time to get the Spark View Engine source code to see if I can get some clues on how to resolve this issue.  After a little debugging, I traced the source of the exception back to ViewLoader.PartialViewFolderPaths.

image

I found that I could move the “if” statement above the two yield statements I no longer got this exception.  But modifying the source code and consequently having a custom build, of Spark.dll, as part of my code generation application was not what I wanted.  At this point I felt that the Sample Projects weren’t enough to help me get started.  So I did a search on stackoverflow and found this entry “Using Spark View Engine in a stand alone application.”  I found two things of interest on this page.  The first interesting thing was that there was apparently another sample project available for review.  Unfortunately though, this project was hidden in this source code solution and not part of the samples solution.  The second interesting think was that the example code was setting a ViewFolder property on the Spark View Engine.  Ah… finally… the missing link.

I changed my code to include setting the ViewFolder property and passed the AddTemplate the template file relative path then everything worked again.  This change also made it so that I no longer needed to put my template files in a Views folder as I’ve previously believed needed to be done.

using System;
using System.IO;
using System.Windows.Forms;
using Spark;
using Spark.FileSystem;
 
namespace SparkViewEngineTest {
    static class Program {
        [STAThread]
        static void Main() {
            FileInfo fi = new FileInfo(@"MyTemplate.spark");
            SparkViewEngine engine = new SparkViewEngine();
            engine.ViewFolder = new FileSystemViewFolder(fi.DirectoryName);
            engine.Settings.PageBaseType = typeof(MyTemplate).FullName;
            
            var descriptor = new SparkViewDescriptor().AddTemplate(fi.Name);
 
            MyTemplate view = (MyTemplate)engine.CreateInstance(descriptor);
            view.MyText = "blah";
 
            StringWriter sw = new StringWriter();
            view.RenderView(sw);
 
            MessageBox.Show(sw.ToString());
        }
    }
 
    public abstract class MyTemplate : AbstractSparkView {
        public string MyText { get; set; }
    }
}

** One thing to note… The ArgumentNullException was still thrown when a full path was passed  to AddTemplate.


Here is the code I ended up with after a little refactoring.

using System;
using System.IO;
using System.Windows.Forms;
using Spark;
using Spark.FileSystem;
 
namespace SparkViewEngineTest {
    static class Program {
        [STAThread]
        static void Main() {
            FileInfo fi = new FileInfo("MyTemplate.spark");
 
            using (SparkViewEngineHelper<MyTemplate> helper = new SparkViewEngineHelper<MyTemplate>(fi.Name)) {
                helper.View.MyText = "Blah";
                MessageBox.Show(helper.ToString());
            }
 
            using (SparkViewEngineHelper<MyTemplate> helper = new SparkViewEngineHelper<MyTemplate>(fi.FullName)) {
                helper.View.MyText = "Blah2";
                MessageBox.Show(helper.ToString());
            }            
        }
    }
 
    public abstract class MyTemplate : AbstractSparkView {
        public string MyText { get; set; }
    }
 
    public class SparkViewEngineHelper<T> : IDisposable
        where T : AbstractSparkView {
 
        private SparkViewEngine engine;
        public T View { get; private set; }
 
        public SparkViewEngineHelper(string templateFile) {
            this.View = this.CreateView(templateFile);
        }
 
        public void ToFile(string file) {
            File.WriteAllText(file, this.ToString());
        }
 
        public override string ToString() {
            StringWriter sw = new StringWriter();
            this.View.RenderView(sw);
            return sw.ToString();
        }
 
        private T CreateView(string templateFile) {
            FileInfo fi = new FileInfo(templateFile);
 
            if (!fi.Exists) {
                throw new FileNotFoundException(templateFile);
            }
 
            this.engine = new SparkViewEngine();
            this.engine.ViewFolder = new FileSystemViewFolder(fi.DirectoryName);
            this.engine.Settings.PageBaseType = typeof(T).FullName;
 
            SparkViewDescriptor descriptor = new SparkViewDescriptor().AddTemplate(fi.Name);
 
            return (T)engine.CreateInstance(descriptor);
        }
 
        void IDisposable.Dispose() {
            if (this.engine != null && this.View != null) {
                this.engine.ReleaseInstance(this.View);
            }
        }
    }
}

November 3, 2009

IQToolKit Data Provider Repository

Filed under: Development — Tags: , — Tom Brothers @ 1:44 am

Inspired by the GenericRepository found in “ASP.NET MVC Framework Unleashed.”, I decided to write a Repository class to work with the IQToolKit Data Providers.

The first step was to create an interface named IRepository.

using System.Linq;
 
namespace IQToolkitContrib {
    public interface IRepository {
        T Get<T>(object id) where T : class;
        IQueryable<T> List<T>() where T : class;
        void Insert<T>(T entity) where T : class;
        void Update<T>(T entity) where T : class;
        void Delete<T>(T entity) where T : class;
    }
}

The second step was to create an abstract class name ARepository.  This class implements the IRepository interface by creating all the methods as abstract methods.  This class also includes a method CreateGetExpression which returns an Expression that will be used to get an Entity Instance by Primary Key.  The Entity’s Primary Key Property Name will be provided by an abstract method named GetPrimaryKeyPropertyName.

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
 
namespace IQToolkitContrib {
    public abstract class ARepository : IRepository {
        protected Expression<Func<T, bool>> CreateGetExpression<T>(object id) {
            ParameterExpression e = Expression.Parameter(typeof(T), "e");
            PropertyInfo pi = typeof(T).GetProperty(this.GetPrimaryKeyPropertyName<T>());
            MemberExpression m = Expression.MakeMemberAccess(e, pi);
            ConstantExpression c = Expression.Constant(id, id.GetType());
            BinaryExpression b = Expression.Equal(m, c);
            Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(b, e);
            return lambda;
        }
 
        protected abstract string GetPrimaryKeyPropertyName<T>();
        public abstract T Get<T>(object id) where T : class;
        public abstract IQueryable<T> List<T>() where T : class;
        public abstract void Insert<T>(T entity) where T : class;
        public abstract void Update<T>(T entity) where T : class;
        public abstract void Delete<T>(T entity) where T : class;
    }
}

The third step was to create a class named DbEntityRepository.  This class inherits from the ARepository class by overriding the abstract methods with IQToolKit Data Provider specific code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using IQToolkit;
using IQToolkit.Data.Common;
 
namespace IQToolkitContrib {
    public class DbEntityRepository : ARepository {
        public DbEntityProviderBase Provider { get; private set; }
 
        public DbEntityRepository(DbEntityProviderBase provider) {
            this.Provider = provider;
        }
 
        protected override string GetPrimaryKeyPropertyName<T>() {
            MappingEntity mappingEntity = this.Provider.Mapping.GetEntity(typeof(T));
            List<MemberInfo> memberInfoList = this.Provider.Mapping.GetPrimaryKeyMembers(mappingEntity).ToList();
 
            if (memberInfoList.Count != 1) {
                throw new ApplicationException(string.Format("Cannot determine the primary key for {0}", typeof(T)));
            }
 
            MemberInfo primaryKeyMemberInfo = memberInfoList[0];
            return primaryKeyMemberInfo.Name;
        }
 
        public override T Get<T>(object id) {
            //// The "FirstOrDefault" will not work with LINQtoVFP.  The following statement would cause an error because it does not include an OrderBy.
            //// return this.List<T>().FirstOrDefault(this.CreateGetExpression<T>(id, primaryKeyMemberInfo.Name));   
 
            List<T> list = this.List<T>().Where<T>(this.CreateGetExpression<T>(id)).ToList();
 
            if (list.Count == 0) {
                return default(T);
            }
 
            return list[0];
        }
        
        public override IQueryable<T> List<T>() {
            return this.GetEntityTable<T>();
        }
 
        public override void Insert<T>(T entity) {
            if (entity is IValidate) {
                ((IValidate)entity).Validate();
            }
 
            this.GetEntityTable<T>().Insert<T>(entity);
        }
        
        public override void Update<T>(T entity) {
            if (entity is IValidate) {
                ((IValidate)entity).Validate();
            }
 
            this.GetEntityTable<T>().Update<T>(entity);
        }
 
        public override void Delete<T>(T entity) {
            this.GetEntityTable<T>().Delete<T>(entity);
        }
 
        private IEntityTable<T> GetEntityTable<T>() {
            return this.Provider.GetTable<T>(this.Provider.Mapping.GetTableId(typeof(T)));
        }
    }
}


At this point, I have developed a very simple Repository to work with the IQToolKit Providers.  This Repository in itself has a few uses but I believe that it would be more useful (at lease for the purpose of TDD) if I added one more layer of abstraction.

So for the fourth step, I created class named DataContext.  This class implements the IRepository interface and has a constructor that is passed an IRepository Instance.  The purpose of this class is to simply wrap up the calls to the IRepository Instance.

(It helps to think of the IRepository Instance as the Data Access Layer and the DataContext as the Business Logic Layer.)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IQToolkitContrib {
    public class DataContext : IRepository {
        private IRepository repository;
        
        public DataContext(IRepository repository) {
            this.repository = repository;
        }
 
        public T Get<T>(object id) where T : class {
            return this.repository.Get<T>(id);
        }
 
        public IQueryable<T> List<T>() where T : class {
            return this.repository.List<T>();
        }
 
        public void Insert<T>(T entity) where T : class {
            this.repository.Insert<T>(entity);
        }
 
        public void Update<T>(T entity) where T : class {
            this.repository.Update<T>(entity);
        }
 
        public void Delete<T>(T entity) where T : class {
            this.repository.Delete<T>(entity);
        }
    }
}

In the final step, I created a class named MemoryRepository.  This class inherits from the ARepository class by overriding the abstract methods to work with a List<object> Instance.  This class can be used to pass into the DataContext while creating tests using TDD.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
 
namespace IQToolkitContrib {
    public partial class MemoryRepository : ARepository {
        private List<object> entities = new List<object>();
 
        protected override string GetPrimaryKeyPropertyName<T>() {
            return Path.GetExtension(typeof(T).FullName).Substring(1) + "Id";
        }
 
        protected virtual void SetIdentityValue<T>(T entity) where T : class {
            PropertyInfo pi = typeof(T).GetProperty(this.GetPrimaryKeyPropertyName<T>());
 
            switch (pi.PropertyType.FullName) {
                case "System.Int32":
                case "System.Int64":
                    long id = Convert.ToInt64(pi.GetValue(entity, null));
 
                    if (id <= 0) {
                        pi.SetValue(entity, this.List<T>().Count() + 1, null);
                    }
 
                    break;
                case "System.String":
                    if (string.IsNullOrEmpty(pi.GetValue(entity, null) as string)) {
                        pi.SetValue(entity, (this.List<T>().Count() + 1).ToString(), null);
                    }
 
                    break;
                case "System.Guid":
                    Guid guidId = (Guid)pi.GetValue(entity, null);
 
                    if (guidId == default(Guid)) {
                        pi.SetValue(entity, Guid.NewGuid(), null);
                    }
 
                    break;
                default:
                    throw new NotImplementedException(string.Format("PropertyType {0} not handled.", pi.PropertyType.FullName));
            }
        }
 
        public override T Get<T>(object id) {
            return this.List<T>().FirstOrDefault(this.CreateGetExpression<T>(id));
        }
 
        public override IQueryable<T> List<T>() {
            return this.entities.OfType<T>().AsQueryable();
        }
 
        public override void Insert<T>(T entity) {
            if (entity is IValidate) {
                ((IValidate)entity).Validate();
            }
 
            this.SetIdentityValue<T>(entity);
            this.entities.Add(entity);
        }
 
        public override void Update<T>(T entity) {
            if (entity is IValidate) {
                ((IValidate)entity).Validate();
            }
 
            string primaryKeyPropertyName = this.GetPrimaryKeyPropertyName<T>();
            PropertyInfo pi = typeof(T).GetProperty(primaryKeyPropertyName);
            object id = pi.GetValue(entity, null);
            T originalEntity = this.Get<T>(id);
 
            var properties = typeof(T).GetProperties();
 
            foreach (var prop in properties) {
                if (prop.CanWrite && prop.Name != primaryKeyPropertyName) {
                    var value = prop.GetValue(entity, null);
                    prop.SetValue(originalEntity, value, null);
                }
            }
        }
 
        public override void Delete<T>(T entity) {
            this.entities.Remove(entity);
        }
    }
}


Source Code and Binaries can be found at IQToolkit Contrib.

October 22, 2009

MSBuild Scripts Reworked

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

About a year ago, I spent some time trying to get up to speed with MSBuild so that I could put together a Continuous Integration Server.  At that time I wasn’t able to find any good MSBuild related books with my Safari Books Online subscription so I spent a fair amount of time searching the internet for examples.  I eventually found everything that I needed to write the MSBuild scripts and custom MSBuild Tasks for the CI Server.  Everything has been working great for some time now.  So wouldn’t you know it that when I don’t need a MSBuild book… I finally see one in the “Just Added” section of Safari Books Online.

Inside the Microsoft® Build Engine: Using MSBuild and Team Foundation Build (PRO-Developer)

Needless to say, I just had to read the book to see what I may have overlooked.  As it turns out, I overlooked three important features… Batching, Transformations, and Well-Known Metadata.  So with this new found knowledge I decided to rewrite my build scripts.  The result of this rewrite was a significant reduction in the number of MSBuild scripts and custom MSBuild Tasks.

What follows is an attempt to document my reworked MSBuild scripts…

My initial goal was to rewrite a build script for an ASP.Net MVC Application.  To test my new build scripts, I created a new MVC Application with one simple modification.  I modified the web.config to use separate files for appSettings and connectionStrings.

image

Here is the final build script (Northwind.msbuild) that I came up with for the MVC Application:

 1: <?xml version="1.0" encoding="utf-8" ?>
 2: <project defaulttargets="UpdateSource;Test;Build;" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
 3:     <Import Project="Resourcesmain.targets" Condition="$(CIPath)==''" />
 4:
 5:     <PropertyGroup>
 6:         <Application>Northwind</Application>
 7:     </PropertyGroup>
 8:
 9:     <Target Name="UpdateSource" DependsOnTargets="SubversionUpdateTarget" />
 10:     <Target Name="Test" DependsOnTargets="MSTestTarget" />
 11:     <Target Name="Rebuild" DependsOnTargets="BuildSolutionTarget" />
 12: </project>

The build script has three local targets… UpdateSource, Test, and Rebuild and relies on three imported targets… SubversionUpdateTarget, MSTestTarget, and RebuildSolutionTarget.  The three imported targets come from an external file “main.targets” which is imported in line #3.  The local targets do not contain any commands to execute.  Instead, they rely on the imported targets that are specified in the DependsOnTargets attribute to do all the work.  The imported targets work based on a naming convention.  This naming convention requires that a property named Application has a value.  In this example, the Application is defined as “Northwind” as you can see in line #6.

As you can see, there really isn’t much to Northwind.msbuild.  All the real code is in main.targets.  So lets dig into this file…

PropertyGroup: 

 1: <propertygroup>
 2:     <!-- ci root directory -->
 3:     <CIPath>c:_CI</CIPath>
 4:     <!-- used for the application build directory -->
 5:     <CITempPath>$(CIPath)Temp</CITempPath>
 6:     <!-- used for the tests build directory and hold the test results -->
 7:     <CITestPath>$(CIPath)Test</CITestPath>
 8:     <!-- deployment directories and zip files -->
 9:     <CIDeployPath>$(CIPath)Deploy</CIDeployPath>
 10:     <!-- ci source code directories -->
 11:     <CISourceCodePath>$(CIPath)Source</CISourceCodePath>
 12:     <!-- TPI source code directories -->
 13:     <TPISourceCodePath>$(CISourceCodePath)TPI</TPISourceCodePath>
 14:
 15:     <!-- This should be set to true by the CI Server to continue past test errors. This will allow the CI Server to parse and report the test results -->
 16:     <MSTestContinueOnError Condition="$(MSTestContinueOnError)==''">false</MSTestContinueOnError>
 17:
 18:     <!-- full path to svn.exe -->
 19:     <SvnExe>C:Program FilesSubversionbinsvn.exe</SvnExe>
 20:     <!-- full path to mstest.exe -->
 21:     <MSTestExe>c:Program FilesMicrosoft Visual Studio 9.0Common7IDEMSTest.exe</MSTestExe>
 22: </propertygroup>

Defines all global properties.

SubversionUpdateTarget: 

 1: <target name="SubersionUpdateTarget" condition="$(Application) != ''">
 2:     <PropertyGroup>
 3:         <AppSourceCodePath Condition="$(AppSourceCodePath) == ''">$(CISourceCodePath)$(Application)</AppSourceCodePath>
 4:     </PropertyGroup>
 5:
 6:     <Exec Command="&quot;$(SvnExe)&quot; update $(AppSourceCodePath)" />
 7: </target>

Executes Svn.exe to update the AppSourceCodePath.

MSTestTarget: 

 1: <target name="MSTestTarget" condition="$(Application) != ''">
 2:     <PropertyGroup>
 3:         <AppSourceCodePath Condition="$(AppSourceCodePath) == ''">$(CISourceCodePath)$(Application)</AppSourceCodePath>
 4:     </PropertyGroup>
 5:
 6:     <!-- get a list of test projects -->
 7:     <ItemGroup>
 8:         <TestProjects Include="$(AppSourceCodePath)****.Tests.csproj" />
 9:     </ItemGroup>
 10:
 11:     <!-- delete the previous test output -->
 12:     <RemoveDir
 13:         Directories="$(CITestPath)%(TestProjects.Filename)"
 14:         Condition="Exists('$(CITestPath)%(TestProjects.Filename)')" />
 15:
 16:     <!-- build the test projects with the output pointing to the CI test directory -->
 17:     <MSBuild
 18:         Projects="%(TestProjects.Identity)"
 19:         Targets="ReBuild"
 20:         Properties="Configuration=Release;OutDir=$(CITestPath)%(TestProjects.Filename)"/>
 21:
 22:     <!-- get a list of test dlls -->
 23:     <ItemGroup>
 24:         <TestDlls Include="$(CITestPath)$(Application)****.Tests.dll" />
 25:     </ItemGroup>
 26:
 27:     <Exec
 28:         Command="&quot;$(MSTestExe)&quot; /testcontainer:&quot;%(TestDlls.Identity)&quot; /resultsfile:&quot;%(TestDlls.FullPath).xml&quot;"
 29:         Condition="%(TestDlls.Identity) != ''"
 30:         ContinueOnError="$(MSTestContinueOnError)" />
 31:
 32:     <!-- move the results files into the root CI test directory-->
 33:     <Copy
 34:         SourceFiles="%(TestDlls.FullPath).xml"
 35:         DestinationFolder="$(CITestPath)"
 36:         Condition="Exists('%(TestDlls.FullPath).xml')" />
 37:
 38:     <!-- remove the project test directories -->
 39:     <RemoveDir Directories="%(TestDlls.RelativeDir)" />
 40: </target>

This target starts by getting a list of all test projects for the application by assuming a simple naming convention of *.Tests.csproj (line #8).  Next, attempts to delete any prior test builds that might exist (line #12) and then builds the test projects (line #17).  Now with all the test projects built, it gets a list of all the test dlls that were built (line #24)… again using a simple naming convention of *.Tests.dll.  Then it executes each test by calling MSTest.exe (line #27).  The results file is moved up a level out of the test build directory into the main CITestPath directory (line #33) and then the test build directory is deleted (line 39).

BuildSolutionTarget:

 1: <target name="BuildSolutionTarget" condition="$(Application) != ''">
 2:     <PropertyGroup>
 3:         <AppSourceCodePath Condition="$(AppSourceCodePath) == ''">$(CISourceCodePath)$(Application)</AppSourceCodePath>
 4:         <AppDeployPath Condition="$(AppDeployPath) == ''">$(CIDeployPath)$(Application)</AppDeployPath>
 5:         <AppTempPath Condition="$(AppTempPath) == ''">$(CITempPath)$(Application)</AppTempPath>
 6:         <DeleteTempDir Condition="$(DeleteTempDir) == ''">true</DeleteTempDir>
 7:     </PropertyGroup>
 8:
 9:     <!-- remove the previous deployment directory -->
 10:     <RemoveDir
 11:         Directories="$(AppDeployPath)"
 12:         Condition="Exists($(AppDeployPath))" />
 13:
 14:     <!-- rebuild -->
 15:     <MSBuild
 16:         Projects="$(AppSourceCodePath)$(Application).sln;"
 17:         Targets="ReBuild"
 18:         Properties="Configuration=Release;WebProjectOutputDir=$(AppDeployPath);OutDir=$(AppTempPath);" />
 19:
 20:     <CallTarget Targets="RenameConfigFilesTarget" />
 21:     <CallTarget Targets="UpdateConfigFilesTarget" />
 22:     <CallTarget Targets="RemoveTempDirTarget" Condition="$(AutoDeleteTempDir) == 'true'" />
 23:     <CallTarget Targets="CreateZipTarget" />
 24: </target>

This target builds the solution with the standard output going into the temp directory and with the web output going to directly to the deployment directory (line #15).  After the build has completed, this targets calls a few targets to create the deployment.

RenameConfigFilesTarget:

 1: <target name="RenameConfigFilesTarget" condition="$(Application) != ''">
 2:     <PropertyGroup>
 3:         <AppDeployPath Condition="$(AppDeployPath) == ''">$(CIDeployPath)$(Application)</AppDeployPath>
 4:         <AppTempPath Condition="$(AppTempPath) == ''">$(CITempPath)$(Application)</AppTempPath>
 5:     </PropertyGroup>
 6:
 7:     <!-- get a list of the config files -->
 8:     <ItemGroup>
 9:         <!-- web applications build directly into $(AppDeployPath) -->
 10:         <ConfigFiles Include="$(AppDeployPath)***.config" />
 11:         <!-- other applications are build into $(AppTempPath) -->
 12:         <ConfigFiles Include="$(AppTempPath)***.config" />
 13:         <!-- don't rename web.config -->
 14:         <ConfigFiles Remove="$(AppDeployPath)**web.config" />
 15:     </ItemGroup>
 16:
 17:     <Copy
 18:         SourceFiles="@(ConfigFiles)"
 19:         DestinationFiles="@(ConfigFiles->'%(FullPath).deploy')" />
 20:
 21:     <Delete Files="@(ConfigFiles)" />
 22: </target>

This target gets a list of all config files in the application deployment directory and the temp directory with the exception of web.confing which is removed from the list.  The files are renamed to include a “.deploy” extension to prevent accidently overwriting these files during the deployment process.

UpdateConfigFilesTarget:

 1: <target name="UpdateConfigFilesTarget" condition="$(Application) != ''">
 2:     <PropertyGroup>
 3:         <AppDeployPath Condition="$(AppDeployPath) == ''">$(CIDeployPath)$(Application)</AppDeployPath>
 4:     </PropertyGroup>
 5:     <ItemGroup>
 6:         <WebConfigFiles Include="$(AppDeployPath)**web.config" />
 7:     </ItemGroup>
 8:
 9:     <XmlUpdate
 10:         XPath="/configuration/system.web/compilation/@debug"
 11:         XmlFileName="%(WebConfigFiles.FullPath)"
 12:         Value="false"
 13:         Condition="%(WebConfigFiles.Identity) != ''" />
 14: </target>

This target gets a list of web.config files (line #6) in the deployment directory and changes the debug flag to false (line #9).

RemoveTempDirTarget:

 1: <target name="RemoveTempDirTarget" condition="$(Application) != ''">
 2:     <PropertyGroup>
 3:         <AppTempPath Condition="$(AppTempPath) == ''">$(CITempPath)$(Application)</AppTempPath>
 4:     </PropertyGroup>
 5:
 6:     <!-- delete the build output temp directory -->
 7:     <RemoveDir
 8:         Directories="$(AppTempPath)"
 9:         Condition="Exists('$(AppTempPath)')" />
 10: </target>

This target deletes the temp directory.

CreateZipTarget:

 1: <target name="CreateZipTarget" condition="$(Application) != ''">
 2:     <PropertyGroup>
 3:         <AppDeployPath Condition="$(AppDeployPath) == ''">$(CIDeployPath)$(Application)</AppDeployPath>
 4:     </PropertyGroup>
 5:     <ItemGroup>
 6:         <Files Include="$(AppDeployPath)***" />
 7:     </ItemGroup>
 8:
 9:     <Zip Files="@(Files)"
 10:         ZipFileName="$(CIDeployPath)$(Application).zip"
 11:         WorkingDirectory="$(AppDeployPath)"
 12:         Condition="%(Files.Identity) != ''" />
 13: </target>

This target creates a zip of the deployment directory.

main.targets:

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Import Project="MSBuild.Community.Tasks.Targets" />

    <PropertyGroup>
        <!-- ci root directory -->
        <CIPath>c:_CI</CIPath>
        <!-- used for the application build directory -->
        <CITempPath>$(CIPath)Temp</CITempPath>
        <!-- used for the tests build directory and hold the test results -->
        <CITestPath>$(CIPath)Test</CITestPath>
        <!-- deployment directories and zip files -->
        <CIDeployPath>$(CIPath)Deploy</CIDeployPath>
        <!-- ci source code directories -->
        <CISourceCodePath>$(CIPath)Source</CISourceCodePath>
        <!-- TPI source code directories -->
        <TPISourceCodePath>$(CISourceCodePath)TPI</TPISourceCodePath>

        <!-- This should be set to true by the CI Server to continue past test errors. This will allow the CI Server to parse and report the test results -->
        <MSTestContinueOnError Condition="$(MSTestContinueOnError)==''">false</MSTestContinueOnError>

        <!-- full path to svn.exe -->
        <SvnExe>C:Program FilesSubversionbinsvn.exe</SvnExe>
        <!-- full path to mstest.exe -->
        <MSTestExe>c:Program FilesMicrosoft Visual Studio 9.0Common7IDEMSTest.exe</MSTestExe>
    </PropertyGroup>

    <Target Name="SubversionUpdateTarget" Condition="$(Application) != ''">
        <PropertyGroup>
            <AppSourceCodePath Condition="$(AppSourceCodePath) == ''">$(CISourceCodePath)$(Application)</AppSourceCodePath>
        </PropertyGroup>

        <Exec Command="&quot;$(SvnExe)&quot; update $(AppSourceCodePath)" />
    </Target>

    <Target Name="MSTestTarget" Condition="$(Application) != ''">
        <PropertyGroup>
            <AppSourceCodePath Condition="$(AppSourceCodePath) == ''">$(CISourceCodePath)$(Application)</AppSourceCodePath>
        </PropertyGroup>

        <!-- get a list of test projects -->
        <ItemGroup>
            <TestProjects Include="$(AppSourceCodePath)****.Tests.csproj" />
        </ItemGroup>

        <!-- delete the previous test output -->
        <RemoveDir
            Directories="$(CITestPath)%(TestProjects.Filename)"
            Condition="Exists('$(CITestPath)%(TestProjects.Filename)')" />

        <!-- build the test projects with the output pointing to the CI test directory -->
        <MSBuild
            Projects="%(TestProjects.Identity)"
            Targets="ReBuild"
            Properties="Configuration=Release;OutDir=$(CITestPath)%(TestProjects.Filename)"/>

        <!-- get a list of test dlls -->
        <ItemGroup>
            <TestDlls Include="$(CITestPath)$(Application)****.Tests.dll" />
        </ItemGroup>

        <Exec
            Command="&quot;$(MSTestExe)&quot; /testcontainer:&quot;%(TestDlls.Identity)&quot; /resultsfile:&quot;%(TestDlls.FullPath).xml&quot;"
            Condition="%(TestDlls.Identity) != ''"
            ContinueOnError="$(MSTestContinueOnError)" />

        <!-- move the results files into the root CI test directory-->
        <Copy
            SourceFiles="%(TestDlls.FullPath).xml"
            DestinationFolder="$(CITestPath)"
            Condition="Exists('%(TestDlls.FullPath).xml')" />

        <!-- remove the project test directories -->
        <RemoveDir Directories="%(TestDlls.RelativeDir)" />
    </Target>

    <Target Name="BuildSolutionTarget" Condition="$(Application) != ''">
        <PropertyGroup>
            <AppSourceCodePath Condition="$(AppSourceCodePath) == ''">$(CISourceCodePath)$(Application)</AppSourceCodePath>
            <AppDeployPath Condition="$(AppDeployPath) == ''">$(CIDeployPath)$(Application)</AppDeployPath>
            <AppTempPath Condition="$(AppTempPath) == ''">$(CITempPath)$(Application)</AppTempPath>
            <DeleteTempDir Condition="$(DeleteTempDir) == ''">true</DeleteTempDir>
        </PropertyGroup>

        <!-- remove the previous deployment directory -->
        <RemoveDir
            Directories="$(AppDeployPath)"
            Condition="Exists($(AppDeployPath))" />

        <!-- rebuild -->
        <MSBuild
            Projects="$(AppSourceCodePath)$(Application).sln;"
            Targets="ReBuild"
            Properties="Configuration=Release;WebProjectOutputDir=$(AppDeployPath);OutDir=$(AppTempPath);" />

        <CallTarget Targets="RenameConfigFilesTarget" />
        <CallTarget Targets="UpdateConfigFilesTarget" />
        <CallTarget Targets="RemoveTempDirTarget" Condition="$(AutoDeleteTempDir) == 'true'" />
        <CallTarget Targets="CreateZipTarget" />
    </Target>

    <Target Name="CreateZipTarget" Condition="$(Application) != ''">
        <PropertyGroup>
            <AppDeployPath Condition="$(AppDeployPath) == ''">$(CIDeployPath)$(Application)</AppDeployPath>
        </PropertyGroup>
        <ItemGroup>
            <Files Include="$(AppDeployPath)***" />
        </ItemGroup>

        <Zip Files="@(Files)"
             ZipFileName="$(CIDeployPath)$(Application).zip"
             WorkingDirectory="$(AppDeployPath)"
             Condition="%(Files.Identity) != ''" />
    </Target>

    <Target Name="RemoveTempDirTarget" Condition="$(Application) != ''">
        <PropertyGroup>
            <AppTempPath Condition="$(AppTempPath) == ''">$(CITempPath)$(Application)</AppTempPath>
        </PropertyGroup>

        <!-- delete the build output temp directory -->
        <RemoveDir
            Directories="$(AppTempPath)"
            Condition="Exists('$(AppTempPath)')" />
    </Target>

    <Target Name="RenameConfigFilesTarget" Condition="$(Application) != ''">
        <PropertyGroup>
            <AppDeployPath Condition="$(AppDeployPath) == ''">$(CIDeployPath)$(Application)</AppDeployPath>
            <AppTempPath Condition="$(AppTempPath) == ''">$(CITempPath)$(Application)</AppTempPath>
        </PropertyGroup>

        <!-- get a list of the config files -->
        <ItemGroup>
            <!-- web applications build directly into $(AppDeployPath) -->
            <ConfigFiles Include="$(AppDeployPath)***.config" />
            <!-- other applications are build into $(AppTempPath) -->
            <ConfigFiles Include="$(AppTempPath)***.config" />
            <!-- don't rename web.config -->
            <ConfigFiles Remove="$(AppDeployPath)**web.config" />
        </ItemGroup>

        <Copy
            SourceFiles="@(ConfigFiles)"
            DestinationFiles="@(ConfigFiles->'%(FullPath).deploy')" />

        <Delete Files="@(ConfigFiles)" />
    </Target>

    <Target Name="UpdateConfigFilesTarget" Condition="$(Application) != ''">
        <PropertyGroup>
            <AppDeployPath Condition="$(AppDeployPath) == ''">$(CIDeployPath)$(Application)</AppDeployPath>
        </PropertyGroup>
        <ItemGroup>
            <WebConfigFiles Include="$(AppDeployPath)**web.config" />
        </ItemGroup>

        <XmlUpdate
            XPath="/configuration/system.web/compilation/@debug"
            XmlFileName="%(WebConfigFiles.FullPath)"
            Value="false"
            Condition="%(WebConfigFiles.Identity) != ''" />
    </Target>
</Project>

One thing that I haven’t mentioned yet was that the main.targets relies on the Zip and UpdateXml Tasks from the MSBuild Community Tasks Project.

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.

August 25, 2009

LINQ to VFP – Example #2

Filed under: Development — Tags: — Tom Brothers @ 12:28 am

My first example was extremely limited in showing what could be accomplished using LINQ to VFP.  For this example, I would like to work though a LINQ to SQL example using LINQ to VFP as the data context.

Basic Setup:

  1. Create a new WebSite.
  2. Add references to IQToolkit.dll, LinqToVfp.dll, IQToolkitContrib.dll, and IQToolkitContrib.Web.dll
  3. Add a Northwind connection string setting to the web.config
    <connectionStrings>
      <add name="northwind" 
           providerName="System.Data.OleDb" 
           connectionString="Provider=VFPOLEDB.1;Data Source=**Your Path**Northwind.dbc;"/>
    </connectionStrings>

  4. Add a page control reference in the web.config to use IQToolkitContrib.Web.DataSource
    <pages>
        <controls>
            <add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>        
            <add tagPrefix="asp" namespace="System.Web.UI.WebControls" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>        
            <add tagPrefix="iqw" namespace="IQToolkitContrib.Web" assembly="IQToolkitContrib.Web" />    
        </controls>
    </pages>

Create Data Classes:

  1. Add a new class: Supplier.cs
    public class Supplier {
        public int SupplierID { get; set; } 
        public string CompanyName { get; set; }
    }
  2. Add a new Class: Category.cs
    public class Category {
        public int CategoryID { get; set; }
        public string CategoryName { get; set; }
    }
  3. Add a new Class: Product.cs
    using System;
    using IQToolkitContrib;
     
    public class Product : IValidate {
        public int ProductID { get; set; }
        public string ProductName { get; set; }
        public int SupplierID { get; set; }
        public Supplier Supplier { get; set; }
        public int CategoryID { get; set; }
        public Category Category { get; set; }
        public string QuantityPerUnit { get; set; }
        public decimal UnitPrice { get; set; }
        public int UnitsInStock { get; set; }
        public int UnitsOnOrder { get; set; }
        public int ReOrderlevel { get; set; }
        public bool Discontinued { get; set; }
     
        public void Validate() {
            if (this.Discontinued && this.UnitsOnOrder > 0) {
                throw new ArgumentException("Reorder level can't be greater than 0 if Discontinued");
            }
        }
    }
  4. Add a new Class: NorthwindPolicy.cs
    using System.Reflection;
    using LinqToVfp;
     
    internal class NorthwindQueryPolicy : VfpQueryPolicy {
        public override bool IsIncluded(MemberInfo member) {
            // this will ensure that the Product.Supplier and Product.Category properties will be populated
            switch (member.Name) {
                case "Supplier":
                case "Category":
                    return true;
                default:
                    return false;
            }
        }
    }
  5. Add a new Class: Northwind.cs
    using System.Configuration;
    using IQToolkit;
    using IQToolkitContrib;
    using LinqToVfp;
     
    public class Northwind : AVfpDatabaseContainer {
        public Northwind()
            : base(ConfigurationManager.ConnectionStrings["northwind"].ConnectionString, null) {
            // update the provider with some loading options
            this.Provider = this.Provider.New(new NorthwindQueryPolicy());
     
            // this will make it so that all command will be logged to the Output window
            this.Provider.Log = new DebuggerWriter();
        }
     
        public IEntityTable<Product> Products {
            get { return this.Provider.GetTable<Product>("Products"); }
        }
     
        public IEntityTable<Supplier> Suppliers {
            get { return this.Provider.GetTable<Supplier>("Suppliers"); }
        }
     
        public IEntityTable<Category> Categories {
            get { return this.Provider.GetTable<Category>("Categories"); }
        }
    }

Page Setup:  I had the resulting Example2.aspx and Example2.cs after working though the LINQ to SQL example with the only change being the replacement of <asp:LinqDataSource with <iqw:DataSource. 

  1. Example2.aspx
  2. <%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Example2.aspx.cs" Inherits="Example2" %>
     
    <!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>
                    Pick Category:
                    <asp:DropDownList ID="CategoryList" 
                                      DataSourceID="CategoryDataSource" 
                                      DataTextField="CategoryName" 
                                      DataValueField="CategoryId" 
                                      AutoPostBack="true" 
                                      runat="server" />
                </div>
                <div style="color:Red;">
                    <asp:Literal ID="ErrorMessage" runat="server" EnableViewState="false" />
                </div>
                <asp:GridView ID="ProductGrid" 
                              runat="server" 
                              AllowPaging="True" 
                              AllowSorting="True" 
                              AutoGenerateColumns="False" 
                              DataSourceID="ProductDataSource" 
                              DataKeyNames="ProductId"
                              OnRowUpdated="ProductGrid_RowUpdated"
                              OnRowDeleted="ProductGrid_RowDeleted">
                    <Columns>
                        <asp:CommandField ShowEditButton="True" />
                        <asp:CommandField ShowDeleteButton="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" 
                                        SortExpression="UnitPrice" />
                        <asp:BoundField DataField="UnitsInStock" 
                                        HeaderText="UnitsInStock" 
                                        SortExpression="UnitsInStock" />
                        <asp:BoundField DataField="UnitsOnOrder" 
                                        HeaderText="UnitsOnOrder" 
                                        SortExpression="UnitsOnOrder" />
                        <asp:CheckBoxField DataField="Discontinued" 
                                           HeaderText="Discontinued" 
                                           SortExpression="Discontinued" />
                    </Columns>
                </asp:GridView>
                <iqw:DataSource ID="ProductDataSource" 
                                runat="server" 
                                ContextTypeName="Northwind" 
                                TableName="Products" 
                                Where="CategoryId == @CategoryId"
                                EnableDelete="true"
                                EnableUpdate="true"
                                EnableInsert="true">
                    <WhereParameters>
                        <asp:ControlParameter ControlID="CategoryList" 
                                              Name="CategoryId" 
                                              PropertyName="SelectedValue" 
                                              Type="Int32" />
                    </WhereParameters>
                </iqw:DataSource>
     
                <iqw:DataSource ID="CategoryDataSource" 
                                runat="server" 
                                ContextTypeName="Northwind" 
                                TableName="Categories" />
     
                <iqw:DataSource ID="SupplierDataSource" 
                                runat="server" 
                                TableName="Suppliers" />    
            </div>
        </form>
    </body>
    </html>
  3. Example2.cs
  4. using System;
    using System.Web.UI.WebControls;
     
    public partial class Example2 : System.Web.UI.Page {
        protected void ProductGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e) {
            if (e.Exception != null) {
                if (e.Exception is ArgumentException) {
                    this.ErrorMessage.Text = e.Exception.Message;
                }
                else {
     
                    this.ErrorMessage.Text = "An error occurred while trying to update this product.";
                }
     
                e.ExceptionHandled = true;
                e.KeepInEditMode = true;
            }
        }
     
        protected void ProductGrid_RowDeleted(object sender, GridViewDeletedEventArgs e) {
            if (e.Exception != null) {
                this.ErrorMessage.Text = e.Exception.Message;
                e.ExceptionHandled = true;
            }
        }
    }

      

      * Note: When attempting to delete a record, you will get a trigger error due to referential integrity. You can create a new record in the Product table for testing the delete link.

    August 23, 2009

    Custom LinqDataSource for IQToolkit

    Filed under: Development — Tags: , — Tom Brothers @ 1:34 am

    I was trying to work though a LINQ to SQL example using IQToolkit as the data context. I found that the LinqDataSource would work fine for reading the data.  But when attempting to update the data an exception was raised indicating the data context did not extent System.Data.Linq.DataContext.  So to finish up the example, I needed to create a custom LinqDataSource for IQToolkit.  After a little inspection using Reflector, I found that I just needed to create two sub-classes.

    The first class that needed to be created was a sub-class of LinqDataSourceView:

    using System;
    using System.Globalization;
    using System.Linq;
    using System.Web;
    using System.Web.UI.WebControls;
    using IQToolkit;
     
    namespace IQToolkitContrib.Web {
        public class DataSourceView : LinqDataSourceView {
            private LinqDataSource owner;
     
            public DataSourceView(LinqDataSource owner, string name, HttpContext context)
                : base(owner, name, context) {
                this.owner = owner;
            }
     
            /// <summary>
            /// Make sure that the data context has a property that implements IEntityTable
            /// </summary>
            protected override void ValidateContextType(Type contextType, bool selecting) {
                if (!selecting && contextType.GetProperties().Where(p => p.PropertyType.GetInterface("IEntityTable") != null).Count() == 0) {
                    throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "The data context used by IQToolkit-DataSourceView '{0}' must have an IEntityTable Property when the Delete, Insert or Update operations are enabled.", this.owner.ID));
                }
            }
     
            /// <summary>
            /// Make sure that the table implementes IEntityTable
            /// </summary>
            protected override void ValidateTableType(Type tableType, bool selecting) {
                if (!selecting && (!tableType.IsGenericType || tableType.GetInterface("IEntityTable") == null)) {
                    throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "The table property used by IQToolkit-DataSourceView '{0}' must extend IEntityTable when the Delete, Insert or Update operations are enabled.", this.owner.ID));
                }
            }
     
            protected override void DeleteDataObject(object dataContext, object table, object oldDataObject) {
                ((IEntityTable)table).Delete(oldDataObject);
            }
     
            protected override void UpdateDataObject(object dataContext, object table, object oldDataObject, object newDataObject) {
                ((IEntityTable)table).Update(newDataObject);
            }
     
            protected override void InsertDataObject(object dataContext, object table, object newDataObject) {
                ((IEntityTable)table).Insert(newDataObject);
            }
        }
    }

    The second class that needed to be created was a sub-class of LinqDataSource:

    using System.Web.UI.WebControls;
     
    namespace IQToolkitContrib.Web {
        public class DataSource : LinqDataSource {
            protected override LinqDataSourceView CreateView() {
                return new DataSourceView(this, "DefaultView", this.Context);
            }
        }
    }

    After building these two classes in a separate assembly, I was able to use the DataSource as I would any other custom server control.

    August 19, 2009

    LINQ to VFP – Example #1

    Filed under: Development — Tags: — Tom Brothers @ 9:13 pm

    The following example is a quick proof of concept simply showing how to view the Customer table from the Northwind.dbc.

    1. Create a new WebSite.
    2. Add references to IQToolkit.dll and LinqToVfp.dll
    3. Add a Northwind connection string setting to the web.config
      <connectionStrings>
        <add name="northwind"
             providerName="System.Data.OleDb"
             connectionString="Provider=VFPOLEDB.1;Data Source=**Your Path**Northwind.dbc;"/>
      </connectionStrings>
    4. Add a new class: Customer.cs
      public class Customer {
          public string CustomerId { get; set; }
          public string CompanyName { get; set; }
          public string ContactName { get; set; }
          public string Address { get; set; }
          public string City { get; set; }
          public string Region { get; set; }
          public string PostalCode { get; set; }
          public string Country { get; set; }
          public string Phone { get; set; }
          public string Fax { get; set; }
      }
    5. Add a new class: Northwind.cs
      using System.Configuration;
      using IQToolkit;
      using LinqToVfp;
      
      
      public class Northwind : AVfpDatabaseContainer {
          public Northwind()
              : base(ConfigurationManager.ConnectionStrings["northwind"].ConnectionString, null) {
              // this will make it so that all command will be logged to the Output windoww
              this.Provider.Log = VfpQueryProvider.CreateDebuggerWriter();
          }
      
      
          public IEntityTable<Customer> Customers {
              get { return this.Provider.GetTable<Customer>("Customers"); }
          }
      }
    6. Modify Default.aspx to include to following in the div tag:
      <asp:GridView ID="mainGrid"
                    runat="server"
                    DataSourceID="LinqDataSource1"
                    AllowPaging="True"
                    AllowSorting="True" />
      
      
      <asp:LinqDataSource ID="LinqDataSource1"
                          runat="server"
                          ContextTypeName="Northwind"
                          TableName="Customers" />
    « Newer Posts

    Blog at WordPress.com.