Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Make a master/detail DataGridView using CSLA DynamicRootList - Part II

4.67/5 (4 votes)
12 Mar 2009CPOL15 min read 21K  
The best kept secrets of CslaGen code generation and customization of generated Business Objects.

Synopsis

This project shows how to have a master/detail DataGridView using the CSLA DynamicRootList (or EditableRootListBase) as the master list object. If you use DynamicRootList for the master list, auto save is a standard feature. This project also shows how to implement auto save on the detail list. As a bonus, you get both lists sorted.

In part I, we explained the problem, discussed some background, analyzed the use cases, and overviewed the database and BO design.

In part II, we will start by detailing the specification of objects on CslaGen. Then, we will see the changes that must be done to the generated files in order to have proper DynamicRootList and DynamicRoot object types. Lastly, we will discuss the BO customization.

If you don't care about CslaGen generation and just want to know about the DataGridView details, skip part II and go to Part III - Handling the DataGridView. Otherwise, I will presume you have a basic knowledge of CslaGen.

7. CslaGen code generation

First of all, let's mention, this started as a very big section filled with redundant information, as there were screenshots of every detail of all CslaGen objects. It was a pain to write and boring to read. On the other hand, due to the lack of documentation of CslaGen, new comers do need all the details they can get. So, I moved the complete collection of screenshots to an appendix, keeping only non-redundant and important information in the main article.

One word about the criteria: these are the parameters used on CRUD operations. I use three kinds:

  • Criteria - It's used to get objects or collections. For editable objects, it's also used to update and delete. For root collections, it's normal to have no properties at all, so it will fetch all objects in the collection. In this case, a FilteredCriteria will be used to allow filtering. They use the same Stored Procedure that will be generated by FilteredCriteria. The usual properties are:

    • root collections - no properties
    • root objects - object ID and row version
    • child collections - parent object ID
    • child objects - object ID and row version
  • CriteriaNew - It's used only to create objects, and it doesn't exist on collections. Usually, it has no properties and the object is created with default values.
  • FilteredCriteria - It's used to get a partial collection. Filtering properties could be the object's name, some date, or date range, etc.

7.1. BrandColl

BrandColl is a collection of Brand objects. It's an EditableRootCollection (that's the CslaGen name for EditableRootList). The "not to be used" name of DynamicRootList is EditableRootListBase and not EditableRootList; different names, different kinds of object. In 8.1. Object type transformation, we will transform this object into a DynamicRootList.

A collection doesn't have value properties as those are defined in the objects the collection is made of. So, we will start by the criteria section.

7.1.1. Criteria section

In order to fetch the collection, you must define a Criteria. Although the Stored Procedure isn't generated (Procedure is set to False), the generated code will use an Stored Procedure so you must specify its name. Note all CreateOptions and DeleteOptions are False or blank.

Fig. 3 - Criteria for BrandColl

Fig. 3 - Criteria for BrandColl

A FilteredCriteria should also be generated. In this particular project, it serves no purpose, but it could prove useful in other situations. Note that the procedure name is the same. The procedure is generated in such a way that it will work with or without filtering parameters. The FilteredCriteria definition is just like Criteria. The only differences are:

  • Procedure is set to True so the Stored Procedure is generated.
  • FilteredCriteria does have properties.

These properties are used to specify the filter or filters to be used. In this example, we will use only the brand name as a filter.

Fig. 4 - Properties of FilteredCriteria for BrandColl

Fig. 4 - Properties of FilteredCriteria for BrandColl

7.1.2. Other properties section

Other BrandColl properties include the Item Type that is set to Brand. This binds Brand objects to the BrandColl collection. The counterpart is in Brand where the Parent Type property is set to BrandColl (Fig. 10).

Fig. 5 - Other properties for BrandColl

Fig. 5 - Other properties for BrandColl

7.2. Brand

Brand is an EditableSwitchable object. In 8.1. Object type transformation, we will transform this object into a DynamicRoot.

EditableSwitchable objects may sometimes be root and sometimes child. On the other hand, Brand is also the parent of ModelColl. The next section is about Child Collection Properties, meaning the children of Brand, i.e., ModelColl. Later on, we will talk about properties of Brand as a child. These properties show up in the Csla Object Info pane under 04. Child Object Options.

7.2.1. Children section

In the Child Collection Properties of Brand, you specify all its children. ModelColl is the child of Brand and you specify it in TypeName. Note the properties:

  • LazyLoad
  • LoadingScheme
  • LoadParameters

When you load the root collection BrandColl, you must load all Brand objects. At this same moment, you could also load all the children and grand children (for each Brand, load its ModelColl and all Models on the collection). This is a lot of objects to load, and setting LazyLoad to True avoids the need to load all these objects.

The property LoadingScheme can be set to SelfLoad, ParentLoad, or None. Although I set it to SelfLoad, you can also set it to ParentLoad as the generated code is exactly the same - at least in this case.

The LoadParameters property specifies what parameter must be passed in order to load a ModelColl. This refers to a criterion of Brand and might be confusing. A property of the parent's criterion is used as a parameter to the child's criterion that will load it. It means that you must have criteria on both parent and child with at least one common property, and in the present case, this property is BrandID.

Fig. 6 - Child Collection Properties for Brand

Fig. 6 - Child Collection Properties for Brand

7.2.2. Value properties section

The Brand object has the following value properties:

  • BrandID
  • BrandName
  • RowVersion

Note that the first and the last properties are read only, as you shouldn't change the values provided by the database layer.

The BrandID property uses System.Threading.Interlocked.Decrement to get a (temporary) default value. The static field _lastID stores the last used ID. This is discussed with some detail in Generating temporary unique ID values for objects.

BrandID has a PrimaryKey property of type DBProvidedPK, and there should be no surprise that the property ReadOnly is set to True. As I said earlier, I use a slightly changed version of CslaGen as you might notice by the FKConstraint property. This feature isn't used in this example.

Fig. 7 - ValueProperties for BrandID (of Brand)

Fig. 7 - ValueProperties for BrandID (of Brand)

The BrandName value property is like BrandID with some differences:

  • PrimaryKey is set to Default
  • DefaultValue is set to blank
  • PropertyType is set to String
  • ReadOnly is set to False

Last, the RowVersion value property is like BrandName with some differences:

  • PropertyType is set to ByteArray
  • ReadOnly is set to True

7.2.3. Criteria section

The Criteria for Brand is also pretty standard. It's used for fetching, updating, and deleting Brand objects, but doesn't take care of creating them as all CreateOptions are False or blank.

Fig. 8 - Criteria for Brand

Fig. 8 - Criteria for Brand

The Criteria for Brand uses two properties: BrandID and RowVersion. This means you need to supply both these parameters in order to fetch, update, or delete a Brand object. Going back to Fig. 6 comments, at least one of these properties must also be used as a Criteria on the child object. The BrandID property fulfils this requirement.

The CriteriaNew for Brand is used just for creating Brand objects and nothing else. All DeleteOptions and GetOptions are False or blank. Of course, it doesn't use any property. As the object doesn't exist yet, it has no ID or row version.

Fig. 9 - CriteriaNew for Brand

Fig. 9 - CriteriaNew for Brand

7.2.4. Other properties section

Other properties of Brand are shown below. Usually, all 04. Child Object Options are very important in order to make it work. As we said before, they refer to the properties of Brand as a child.

  • Lazy Load is set to False.
  • Parent Insert Only is set to False.
  • Parent Properties is blank.
  • The property Parent Type is the complement of the property Item Type in BrandColl (Fig. 5).

This might confuse you (I know it confused me!), but in fact, it's quite correct. This refers to the properties of the object Brand, and this object isn't supposed to be lazy loaded (otherwise, there would be no data to display in the DataGridView). What we said before in Child Collection Properties for Brand (Fig. 6) is that the child of Brand is to be lazy loaded. This means ModelColl is to be lazy loaded.

Fig. 10 - Other Brand properties

Fig. 10 - Other Brand properties

7.3. ModelColl

ModelColl is a collection of Model objects. It's an EditableChildCollection (that's the CslaGen name for EditableChildList). Again, we will start by the criteria section as a collection has no value properties.

7.3.1. Criteria section

ModelColl must have a Criteria where we specify how it will be fetched by its Brand parent. Only the GetOptions are set and all CreateOptions and DeleteOptions are False or blank. The only difference to what shows in Fig. 3 is Procedure that is set to True so the Stored Procedure is generated. Of course, you could use a FilteredCriteria as we did for BrandColl.

The BrandID property is used to fetch the ModelColl as specified in CriteriaProperties. Going back to Fig. 6 comments, the BrandID property must also be used as a Criteria on the parent object.

7.3.2. Other properties section

Other properties of ModelColl are shown below. As usual, all 04. Child Object Options are very important in order to make it work.

  • The property Lazy Load is similar to the property LazyLoad in Child Collection Properties of Brand (Fig. 6). We want ModelColl to be lazy loaded (re-read the comments on Fig. 10).
  • The property Parent Properties is set to BrandID and is the complement of the property LoadParameters in Child Collection Properties of Brand (Fig. 6). The latter property is set to Criteria.BrandID (here Criteria refers to a Brand criteria). This property says how to bind the ModelColl collection to the parent object.
  • The property Parent Type is the complement of the property TypeName in Child Collection Properties of Brand (Fig. 6). This property says who the parent object of ModelColl is.
  • Add Parent Reference is set to False. I guess it could be set to True if you need to have a reference to your Brand parent object in ModelColl.
  • Item Type is set to Model.

Fig. 11 - Other ModelColl properties

Fig. 11 - Other ModelColl properties

7.4. Model

Model is an EditableChild object and has no children. We will start by the value properties.

7.4.1. Value properties section

The Model object has the following value properties:

  • ModelID (like BrandID on Brand)
  • ModelName (like BrandName on Brand)
  • Price
  • RowVersion (like RowVersion on Brand)

The Price property data type is defined as money under SQL Server, and in the .NET Framework, it "translates" to Decimal.

You may find it difficult to understand why the BrandID property isn't included in the Values Properties collection. No need for this as the Csla framework takes care of adding a Parent property as soon as you call MarkAsChild in the default constructor.

How is BrandID stored in the Models table? As you will see in Fig. 12, in 04. Child Object Options, the Parent Properties references BrandID. And, CslaGen generates the Stored Procedure accordingly.

If you add the BrandID property to the Values Properties collection, you will end up with duplicates. To avoid these duplicates, I tried a smart move: I added the BrandID property to the Values Properties collection and set Parent Properties to blank. The problem was that the BrandID value was always the default value and not the correct ID of the parent Brand I was expecting.

7.4.2. Criteria section

This section is very similar to the Brand criteria and you can refer to 7.2.3. Criteria section.

The Criteria for Model is used for fetching, updating, and deleting Model objects, but doesn't take care of creating them as all CreateOptions are False or blank. The Criteria for Model uses two properties: ModelID and RowVersion. This means you need to supply both these parameters in order to fetch, update, or delete a Model object.

The CriteriaNew for Model is used just for creating Model objects and nothing else. You can see all DeleteOptions and GetOptions are False or blank. Of course, it doesn't use any property. As the object doesn't exist yet, it has no ID or row version.

7.4.3. Other properties section

Other properties of Model are shown below. As I said before, all 04. Child Object Options are very important in order to make it work.

  • The property Lazy Load is set to False as otherwise we would have no data to show on the detail DataGridView.
  • Concerning Parent Properties, refer to the final comment in 7.4.1. Value properties section. Refer also to comments in Fig. 11. In a nutshell, this property is set to BrandID. It binds the child object Model to the grand parent object Brand through ModelColl:
  • Model (Parent Properties = BrandID) -> ModelColl (Parent Properties = BrandID) -> Brand (BrandID))

  • The property Parent Type is the complement of the property Item Type in ModelColl (Fig. 11).

Fig. 12 - Other Model properties

Fig. 12 - Other Model properties

8. Changes to the generated code

In the Project Properties tab of CslaGen, you must check Use ".Designer" in file names. This way, CslaGen will generate two files per object (both public partial class files):

  • the <Object Name>.Designer.cs file is always generated
  • the <Object Name>.cs file is generated only when it doesn't exist

The intended use is to make all your customizations on the non .Designer file so customizations aren't wiped out every time you make changes to any object and need to re-generate the project.

8.1. Object type transformation

In order to transform the CslaGen objects EditableRootCollection + EditableSwitchable into the more convenient DynamicRootList + DynamicRoot (CSLA template naming), there are two levels of changes that can be done. The minimal changes involve:

  • change the base class of BrandColl
  • make sure Brand is never a child
  • adding an AddNewCore method to BrandColl

while full changes involve cleaning the useless code.

Full changes mean cleaning all traces of the Brand object "switchability", including of couple of if statements and deleting un-needed classes:

  • one of the overloads of GetBrand on Brand.Designer.cs.
  • DeleteBrand on Brand.Designer.cs.
  • GetBrandChild on Brand.Designer.cs.
  • NewBrandChild on Brand.Designer.cs.
  • DataPortal_Update on BrandColl.Designer.cs.
  • DataPortal_Delete on BrandColl.Designer.cs.

We won't analyze these changes any further. The untouched generated files are included in CslaERLB1.zip as well as Brand.Designer.cs.minimal so you can compare the files to find out the exact differences.

Starting with minimal changes, in BrandColl.Designer.cs, you need to change the base class from BusinessListBase<BrandColl, Brand> to EditableRootListBase<Brand> and comment some lines as shown below.

8.1.1. Changes to BrandColl.Designer.cs

C#
// public partial class BrandColl : BusinessListBase<BrandColl, Brand>
public partial class BrandColl : EditableRootListBase<Brand>

(...)

public void Add()
{
    // Brand brand = Brand.NewBrandChild();
    Brand brand = Brand.NewBrand();
    Add(brand);
}
*/

8.1.2. Changes to Brand.Designer.cs

In Brand.Designer.cs, you need to comment just one line.

C#
internal static Brand GetBrand(SafeDataReader dr)
{
    Brand obj = new Brand();
    // obj.MarkAsChild();
    obj.Fetch(dr);
    obj.MarkOld();
    obj.ValidationRules.CheckRules();
    return obj;
}

8.1.3. Changes to BrandColl.cs

In BrandColl.cs, you need to:

  • add the AddNewCore method, as the DataGridView requires it.
  • comment the Initialize method, as the DynamicRootList base class doesn't define this method.

So. there is nothing to override.

C#
#region Factory Methods

///<summary>Adds a new item to the end of the collection.</summary>
///<returns>The item that was added to the collection.</returns>
///<exception cref="T:System.InvalidCastException">The new item
///is not the same type as the objects contained in the
//<see cref="T:System.ComponentModel.BindingList`1" />.</exception>
protected override object AddNewCore()
{
    Brand item = Brand.NewBrand();
    Add(item);
    return item;
}

#endregion

(...)

/*
protected override void Initialize()
{
    //this.FetchPre += new EventHandler(OnFetchPre);
    //this.FetchPost += new EventHandler(OnFetchPost);
}
*/

8.2. BO customization

8.2.1. Changes to Brand.cs

These changes concern the validation rules only. CSLA 2.1 introduced several improvements in this area, namely RuleSeverity. A word of warning concerning the DataGridView: it uses its own internal error provider and not the standard ErrorProvider. There is an enhanced ErrorWarnInfoProvider that takes advantage of this feature and shows the different icons according to the severity, but you can't use it with the DataGridView.

Since CSLA 3.0, you can specify a PropertyFriendlyName, and brandNameRules is used for that exact purpose.

It would be nice to have a CommonRules for NoDuplicates. That is possible since each editable child has a reference to its parent collection (as soon as you Add it to the collection). Note the use of ToUpperInvariant to make sure the match is case insensitive.

C#
#region Validation Rules

protected override void AddBusinessRules()
{
    var brandNameRules = new RuleArgs("BrandName", "Brand name");
    ValidationRules.AddRule(CommonRules.StringRequired, brandNameRules);
    ValidationRules.AddRule(CommonRules.StringMaxLength,
        new CommonRules.MaxLengthRuleArgs(brandNameRules.PropertyName, 
            brandNameRules.PropertyFriendlyName, 20));
    ValidationRules.AddRule<Brand, RuleArgs>(NoDuplicates, brandNameRules);
}

private static bool NoDuplicates<T>(T target, RuleArgs e) where T : Brand
{
    // no two brands can have the same name; match is case insensitive

    var parent = (BrandColl)target.Parent;
    if (parent != null)
    {
        foreach (var item in parent)
            if (item.BrandName.ToUpperInvariant() == 
                target.BrandName.ToUpperInvariant() && 
                !ReferenceEquals(item, target))
            {
                e.Description = e.PropertyFriendlyName + " must be unique.";
                return false;
            }
    }
    return true;
}

#endregion

8.2.2. Changes to ModelColl.cs

In this file, all you need to do is:

  • Add a AddNewCore method so the DataGridView can save each row as soon as you leave the row.

The code involved is basically the same presented in 8.1.3. Changes to BrandColl.cs

8.2.3. Changes to Model.cs

In this file, you need to add the validation rules section:

  • Add a AddBusinessRules method
  • Add a NoDuplicates method

The code involved is basically the same presented in 8.2.1. Changes to Brand.cs. Note that the uniqueness of the model name is checked for the parent Brand only as specified by the use cases.

8.3 Stored procedures changes

The generated stored procedure code is almost right, except it should delete all models of a brand before deleting the brand itself. To correct that, edit Brand.sql and just before:

SQL
/* Delete object from Brands */

add these lines:

SQL
/* Delete child object from Models */
DELETE FROM [Models]
WHERE         [BrandID] = @BrandID

The untouched generated code is included in CslaERLB1.zip.

This is the solution adopted on the ProjectTracker sample. It can be avoided if the Foreign Key specification is ON DELETE CASCADE. We can argue about each solution's speed. I prefer an explicit statement in the Stored Procedure because it's more transparent and also more portable. CslaGen doesn't try to solve this problem and you must solve it yourself.

One interesting point to note is why this problem exists. When you delete an object, CSLA starts by deleting it from the database and then deletes the BO itself along with all its children.

Other parts of this article:

History

  • Document version 1: 12 March 2009.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)