Validating Collections of Entities (Sets of Data) in LightSwitch

Validating Collections of Entities (Sets of Data) in LightSwitch

  • Comments 11

One of the many challenging things in building n-tier applications is designing a validation system that allows running rules on both the client and the server and sending messages and displaying them back on the client. I’ve built a couple application frameworks in my time and so I know how tricky this can be. I’ve been spending time digging into the validation framework for LightSwitch and I have to say I’m impressed. LightSwitch makes it easy to write business rules in one place and run them in the appropriate tiers. Prem wrote a great article detailing the validation framework that he posted on the LightSwitch Team Blog yesterday that I highly recommend you read first:

Overview of Data Validation in LightSwitch Applications.

Most validation rules you write are rules that you want to run on both the client and the server (middle-tier) and LightSwitch does a great job of handling that for you. For instance when you put a validation rule on an entity property this rule will run first on the client. If there is an error the data must be corrected before it can be saved to the middle-tier. This gives the user an immediate response but also makes the application scale better because you aren’t unnecessarily bothering the middle-tier. Once the validation passes on the client, it is run again on the middle-tier. This is best practice when building middle-tiers - don’t ever assume data coming in is valid.

Validating sets (or collections) of data can get tricky. You usually want to validate the set on the client first but then you have to do it again on the middle-tier, not only because you don’t trust the client, but also because the set of data can change in a multi-user environment. You need to take the change set of data coming in from the client, merge it with the set of data stored in the database, and then proceed with validation. Dealing with change sets and merging yourself can get pretty tricky sometimes. What I didn’t realize at first is that LightSwitch also handles this for you.

Example – Preventing Duplicates

Let’s take an example that I was working on this week. I have the canonical OrderHeader --< OrderDetails >—Product data model. I want a rule that makes sure no duplicate products are chosen across OrderDetail line items on any given order. So if a user enters the same product twice on an order, validation should fail. Here I have an orders screen that lets me edit all the orders for a selected customer. For each order I should not be allowed to enter the same product more than once:

image

Where Do the Rules Go?

You can write rules in xxx_Validate methods for entity properties (fields) and the entity itself. From the Entity Designer select the property name then click the arrow next to the “Write Code” button to drop down the list of available methods. The property methods will display for the selected property. The entity methods are under “General Methods”. In my example if you select the Product property and drop down the list of methods you see two validation methods Product_Validate and OrderDetails_Validate.

image

The Property Methods change as you select an entity property (field) in the designer but the General Methods are always displayed for the entity you are working with. Property _Validate methods run both on the client and then again on the middle-tier. Entity _Validate methods run on the server, these are called DataService validations.

In my order entry scenario I was first tempted to write code in the DataService on the OrderHeader entity and check the collection of OrderDetails there. When I select the OrderHeader entity in the Entity Designer, click the arrow next to the “Write Code” button, and select OrderHeaders_Validate, a method stub is generated for me in the ApplicationDataService class. This is where I was thinking I could validate my set of OrderDetails and return an error if there were duplicates.

Public Class ApplicationDataService

    Private Sub OrderHeaders_Validate(ByVal entity As OrderHeader, ByVal results As EntitySetValidationResultsBuilder)
        Dim isValid = False
        'Write code to validate entity.OrderDetails collection
        '....
        If Not isValid Then
            results.AddPropertyError("There are duplicated products on the order")
        End If
    End Sub
End Class

However I quickly realized that this wouldn’t work because the OrderHeader entity would need to be changed for this validation to fire. If a user is editing a current order’s line items (OrderDetails) then only the validation for the OrderDetail would fire, not OrderHeader. Another issue with putting my rule in the ApplicationDataService class is the user would have to click save before the rule would fire and we’d have an unnecessary round-trip to the middle-tier. We want to be able to check this set for problems on the client first. Another issue is if I found an error then only a general validation message on the order would be presented to the user. They would have to stare at the screen to figure out the problem.

I think the reason why I went this route in the first place is because I was thinking I needed to merge the change set coming from the client with the set of data in the database and then validate that. It turns out that LightSwitch handles this for you. When you are validating a set of data (entity collection) on the client, you are validating what is on the user’s screen. When the validation runs on the server you are validating the merged set of data. NICE!

(Note that you can still access the change set via the DataWorkspace object but we’ll dive into that in a future post. )

The Right Way to Write this Rule

Since LightSwitch is doing all the heavy-lifting for me this rule gets a whole lot easier to implement. Since we’re checking duplicate products on each OrderDetail we need to put the code in the Product_Validate method of the OrderDetail entity (see screenshot above). Now we can write a simple LINQ query to check for duplicates.

Public Class OrderDetail

    Private Sub Product_Validate(ByVal results As EntityValidationResultsBuilder)

        If Me.Product IsNot Nothing Then

            'Look at all the OrderDetails that: 
            '   1) have a product specified (detail.Product IsNot Nothing)
            '   2) have the same product ID as this entity (detail.Product.Id = Me.Product.Id)
            '   3) is not this entity (detail IsNot Me)
            Dim dupes = From detail In Me.OrderHeader.OrderDetails
                          Where detail.Product IsNot Nothing AndAlso
                                detail.Product.Id = Me.Product.Id AndAlso
                                detail IsNot Me

            'If Count is greater than zero then we found a duplicate
            If dupes.Count > 0 Then
                results.AddPropertyError(Me.Product.ProductName + " is a duplicate product")
            End If
         End If
    End Sub
End Class

This validation will fire for every line item we add or update on the order. It will first fire on the client and Me.OrderHeader.OrderDetails will be the collection of line items being displayed on the screen. If this rule passes validation on the client then it will fire on the middle-tier and the Me.OrderHeader.OrderDetails will be the collection of line items that were sent from the client merged with the data on the server. This means that if another user has modified the line items on the order we can still validate this set of data properly. Also notice when we specify the error message, it is attached to the Product property on the OrderDetail entity so when the user clicks the message in the validation summary at the top of the screen, the proper row in the grid is highlighted for them.

image

Stay tuned for more How Do I videos on writing business rules.

UPDATE: Here's a video I did on writing business rules: How Do I: Write business rules for validation and calculated fields in a LightSwitch Application?

Enjoy!

Leave a Comment
  • Please add 8 and 8 and type the answer here:
  • Post
  • The right way is to use the Any() extension method, it costs less performance than the Count() since it only moves the iterator once, and if the MoveNext is true it returns false, unlike Count that iterates thru all.

    Dim hasError = Me.OrderHeader.OrderDetails.Any(

     Function(detail) detail.Product IsNot Nothing AndAlso

                      detail IsNot Me AndAlso

                      detail.Product.Id = Me.Product.Id)

    I also replaced the order of the last two conditions, since the 3rd cond. depends in the 2nd one.

    HTH

    Thanks ez for your post.

    I am eagerly waiting for LightSwitch to grow up and become more robust so it can be used for enterprise business applications and professional development scenarios as well.

  • Thanks Beth. As usual, your timing is perfect.

    @Shimmy - thanks for the great heads-up. I'd be interested in hearing more about some of the potential performance issues like these.

    Cheers!  

  • Thanks Shimmy,

    Though in this case there aren't that many items in the collection ever so the perf difference wouldn't be that much. Also I like to avoid using lambda expressions if possible for better readability. In this case it would be easier to read if we just used the Any extension on the query results:

    If dupes.Any Then

        results.AddPropertyError(Me.Product.ProductName + " is a duplicate product")

    End If

    Cheers,

    -Beth

  • Beth, can we validate against the same collection that the entity belongs to? For example, if we want to retain the old value to do some kind of comparison. E.g. Validate the entered date is not more than n days greater than the existing date value.

    I see there is a possibility of doing this in the ApplicationDataService, but what about directly in the entity class itself?

    Danke!

  • Hi Paul,

    Yes, you can access the original values by drilling into the entity.Details.Properties.[propname].OriginalValue

    You just gave me a great idea for my next post on validation. ;-)

    Cheers,

    -B

  • Awesome, thanks!

    Cheers right back at'cha

    -Paul

  • Anyone translate this into C#? Many thanks

  • Hi Simon,

    You can use this free online translator here:

    www.developerfusion.com/.../vb-to-csharp

  • Hi Beth,

    thanks for your clear video and comments.

    I'm trying to follow your example in a simple application with LightSwitch beta2, but I'm not able to pass an error using linq sintax.

    I've a simple Table called "Group", with 3 properties:  Id, Decription (string), IsCommon (boolean). I need to validate data so that the flag isCommon is set to true only for 1 record

    I write the following lines of codes:

    Namespace LightSwitchApplication

       Public Class Group

           Private Sub isCommon_Validate(results As EntityValidationResultsBuilder)

               ' results.AddPropertyError("<Error-Message>")

               If Me IsNot Nothing Then

                   Dim c As Integer = Aggregate flg In Me Into count(Me.isCommon = True)

                   If c > 1 Then

                       results.AddPropertyError("Error message...")

                   End If

               End If

           End Sub

       End Class

    End Namespace

    But about "Me" (aka Group class, it isn't right?) LS says:

    Expression of type 'LightSwitchApplication.Group' is not queryable. Make sure you are not missing an assembly reference and/or namespace import for the LINQ provider.

    But I see that is not possibile add references in LS for "System.Linq", "System.Data.Linq" and "System.Xml.Linq" namespaces than are necessary as written in msdn.microsoft.com/.../bb763092.aspx

    Can you help me?

    Thanks in advance

    Alex

  • Hi Beth

    Can you please provide the C# code for the row validation

  • Hi Beth, here is a question for you! What happens to your code when you have multiple unsaved entities with duplicate values? Since the changes are not part of the EntitySet, and we can only reference the current changed object being validated with this, how would you for example detect duplicates within the unsaved changes? For example we add a row that for "Pear" then without saving we add another row for "Pear".

    The problem I'm facing is slightly different, but it comes down to the same issue. Where can I access the collection of the EntitySet that has been merged with the unsaved changes?

Page 1 of 1 (11 items)