How to correctly handle validation errors with LLBLGenProDataSource2

Posts   
 
    
AlbertK
User
Posts: 44
Joined: 23-Dec-2009
# Posted on: 23-Dec-2009 18:15:32   

I have the following setup: 1. DataAdapter model with a certain entity that I need to save 2. Validator class that throws ORMEntityValidationException from ValidateEntityBeforeSave 3. A web page with LLBLGenProDataSource2 data source with CacheLocation=Session and LivePeristence=true 4. Telerik 2009.3 grid bound to LLBLGenProDataSource2 data source with auto-generated popup edit form

If I edit an existing record and click Update on the auto-generated grid form, validator throws the exception, which is handled by the grid's ItemUpdated event. The problem is that if the user then hits Cancel to abandon the change, the grid displays the updated value and does not revert back to the original value.

On the data source I see EntityUpdating firing and EntityUpdated is not firing, which makes sense. Also, the update does not save anything in the database, as expected. Is it possible that LLBLGenProDataSource2 is caching the changed value even though the entity was not saved? I tried rebinding the grid to the data source when user hits Cancel, but that didn't help. What is the proper way to handle a situation like this?

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 24-Dec-2009 04:48:49   

Hi Albert,

Let me understand what are you doing: 1. The user clicks Edit in the Grid 2. The user clicks Update in the Grid, so the save process start 3. In your validator you throw an exception 4. You interpcept the exception in your grid event and cancel the update 5. The grid is still in Edit mode. Now the user clicks Cancel 6. The field value doesn't back to its original value.

If that flow is correct, then I think that the entity value has changed already, so it can't back to its original value. All depends on how are you doing this. Please post the relevant code (declarative ASPX and your relevant code behind, including the validation code).

Also please post your LLBLGen runtime library version. (http://www.llblgen.com/TinyForum/Messages.aspx?ThreadID=7720)

David Elizondo | LLBLGen Support Team
AlbertK
User
Posts: 44
Joined: 23-Dec-2009
# Posted on: 28-Dec-2009 16:15:10   

I'm using 2.6 runtime.

Your understanding is correct, except for step 4. In telerik grid, I cannot cancel the update. Instead I get an ItemUpdated event that contains the exception. So I handle it like this, where HandleInsertUpdateException is my function that deals with printing the error message.


    private static void grid_ItemUpdated(object source, GridUpdatedEventArgs e)
    {
        if (e.Exception != null)
        {
            HandleInsertUpdateException((source as RadGrid), e, false);

            e.ExceptionHandled = true;
            e.KeepInEditMode = true;            
        }
    }

My LLBLGENProDataSource2 control is initialized as follows:


    public static void Initialize(LLBLGenProDataSource2 ds, Type entityFactoryType)
    {
        ds.AdapterTypeName = typeof(DataAccessAdapter).AssemblyQualifiedName;
        ds.DataContainerType = DataSourceDataContainerType.EntityCollection;
        ds.EntityFactoryTypeName = entityFactoryType.AssemblyQualifiedName;
        
        ds.CacheLocation = DataSourceCacheLocation.Session;

        ds.DataBinding += new EventHandler(ds_DataBinding);
        ds.EntityUpdated += new EventHandler<DataSourceActionEventArgs>(ds_EntityUpdated);
        ds.EntityUpdating += new EventHandler<CancelableDataSourceActionEventArgs>(ds_EntityUpdating);
        
        ds.EnablePaging = false;

        //LivePersistence causes LLBLGenProDataSource2 to auto-generate insert/update/delete statements, 
        //which saves a lot of manual effort.
        ds.LivePersistence = true;
    }

In my DataAccessAdapter, I override SaveEntity as follows:


        public override bool SaveEntity(IEntity2 entityToSave, bool refetchAfterSave, IPredicateExpression updateRestriction, bool recurse)
        {
            try
            {
                return base.SaveEntity(entityToSave, refetchAfterSave, updateRestriction, recurse);
            }
            catch (ORMQueryExecutionException ORMex)
            {
                int errorNumber = (int)ORMex.DbSpecificExceptionInfo[ExceptionInfoElement.ErrorNumber];

                //Check for primary key violations and unique constraint violations
                if (errorNumber == 2627 || errorNumber == 2601)
                    throw new ORMEntityValidationException("Duplicate record found.", entityToSave);
                else
                    throw ORMex; //rethrow
            }
            //Other exceptions should bubble up
        }

And my validator looks like this:


    [Serializable]
    [DependencyInjectionInfo(typeof(TradeRestrictionEntity), "Validator", ContextType = DependencyInjectionContextType.NewInstancePerTarget)]
    public class TradeRestrictionValidator : ValidatorBase
    {
        public override void ValidateEntityBeforeSave(IEntityCore involvedEntity)
        {
            TradeRestrictionEntity e = (TradeRestrictionEntity)involvedEntity;
            
            // Validate date ranges
            if (e.ExpiryDate.HasValue && (e.StartDate > e.ExpiryDate))
                throw new ORMEntityValidationException("Start date cannot be greater than expiry date.", e);

            // Validate overlapping times
            using (DataAccessAdapter adapter = new DataAccessAdapter())
            {
                LinqMetaData md = new LinqMetaData(adapter);
                
                IQueryable<TradeRestrictionEntity> q = from t in md.TradeRestriction
                        where t.AcctCode == e.AcctCode
                        && 
                        (
                            (t.StartDate == e.StartDate) ||
                            (t.StartDate < e.StartDate && (t.ExpiryDate == null || t.ExpiryDate >= e.StartDate)) ||
                            (t.StartDate > e.StartDate && (e.ExpiryDate == null || t.StartDate <= e.ExpiryDate))
                        )
                        select t;

                if (!e.IsNew) {
                    q = q.Where(x => x.TradeRestrictionId != e.TradeRestrictionId);
                }

                if (q.Count() > 0)
                    throw new ORMEntityValidationException("Another trade restriction overlaps with supplied date range for the same account.", e);
            }
        }
    }

When the user presses cancel, even if I call Databind on LLBLGenDataSource2 or on the grid, the user updated data is still displayed in the grid. The only way to get original data to display back is to do a full refresh of the page by navigating to the page directly even within the same ASP.NET session.

I cannot seem to understand whether it's the LLBLGenDataSource2 control that incorrectly caches the data or whether it's the grid keeping the data in its viewstate or I'm just doing something very wrong. Thank you.

daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 29-Dec-2009 06:08:56   

Isn't a RowUpdating event in the Telerik ASP.Net Grid? Anyway, I will try to find a solution for your situation. Could you please tell me your runtime library version? (follow the link I posted above) as the number you gave me is the major version not the runtime one.

David Elizondo | LLBLGen Support Team
AlbertK
User
Posts: 44
Joined: 23-Dec-2009
# Posted on: 29-Dec-2009 16:00:52   

The runtime is 2.6.9.305.

Unfortunately, there's no RowUpdating event on telerik's RadGrid.

What is the best way to dump the content of the LLBLGenProDataSource2 to check whether the data indeed got updated in its internal cache even when exception was raised downstream?

Thanks.

AlbertK
User
Posts: 44
Joined: 23-Dec-2009
# Posted on: 29-Dec-2009 17:51:13   

Here's additional piece of information. After the exception, occurs, in my grid's ItemUpdated event, I'm able to inspect the internal state of the data source.

((source as RadGrid).MasterTableView.DataSourceObject as LLBLGenProDataSource2).EntityCollection

The DirtyEntities collection has 1 item that contains the data inputed through the grid edit form.

On one hand, I see how the data source needs to have the data even if it didn't get saved, so that the grid edit form can bind to that data to display it after postback. On the the other hand, when the user hits cancel, I think I want to tell the data source to clear the dirty items and somehow revert back - perhaps a full refetch from database would be needed.

Your thoughts/suggestions are much appreciated.

AlbertK
User
Posts: 44
Joined: 23-Dec-2009
# Posted on: 29-Dec-2009 19:05:31   

I found couple of solutions, but neither one seems perfect.

  1. On ItemUpdated event after determining that exception was raised, I can set Refetch on the datasource to true. This clears the "bad" data from the datasource. However, the edit form simply reverts back to old values, which is not an ideal user experience.

  2. I can set Refetch to true when user hits cancel. This seems better because after Update user still sees the data they entered and can correct the values. The problem with this approach is that the user could simply navigate away from page (go backward and then forward) without ever hitting the Cancel button and the bad data is still cached in the data source and gets displayed.

An ideal solution would be such that the edit form that the grid generates simply displays the values that got entered WITHOUT binding to datasource, and the datasource never caches the bad values (or alternatively the datasource can refetch the data).

I'll post what I can think of, but I can't imagine I'm the only one facing the same problem

Walaa avatar
Walaa
Support Team
Posts: 14993
Joined: 21-Aug-2005
# Posted on: 30-Dec-2009 09:43:12   
  1. I can set Refetch to true when user hits cancel. This seems better because after Update user still sees the data they entered and can correct the values. The problem with this approach is that the user could simply navigate away from page (go backward and then forward) without ever hitting the Cancel button and the bad data is still cached in the data source and gets displayed.

Using the Back and Forward usualy leads to unwanted results. What about using the ViewState as the CacheLocation of the datasource for this particular scenario?

AlbertK
User
Posts: 44
Joined: 23-Dec-2009
# Posted on: 30-Dec-2009 15:43:06   

Walaa, thank you for the suggestion. Setting CacheLocation to ViewState indeed fixes the navigation problem I discussed, but at the expense of transfering more data between the server and the client.

So I think I have the workaround.

  1. Reload the datasource whenever user hits Cancel in the grid edit form.
  2. Use ViewState for datasource cache location.

    private void grid_CancelCommand(object source, GridCommandEventArgs e)
    {
        ((source as RadGrid).MasterTableView.DataSourceObject as LLBLGenProDataSource2).Refetch = true;
    }

Out of curiosity, if I'm allowing users to edit one record at a time, is there a more efficient way to refresh the datasource other than reloading it completely? Does datasource have a copy of previous versions of the fields? For example, is there way to tell the datasource to revert all dirty entities to their previous state? I was able to identify which entity in the datasource's EntityCollection was dirty and tried calling RollbackFields and DiscardSavedFields but that didn't seem to work. Thanks.

Walaa avatar
Walaa
Support Team
Posts: 14993
Joined: 21-Aug-2005
# Posted on: 30-Dec-2009 16:35:32   

Out of curiosity, if I'm allowing users to edit one record at a time, is there a more efficient way to refresh the datasource other than reloading it completely? Does datasource have a copy of previous versions of the fields? For example, is there way to tell the datasource to revert all dirty entities to their previous state? I was able to identify which entity in the datasource's EntityCollection was dirty and tried calling RollbackFields and DiscardSavedFields but that didn't seem to work. Thanks.

You can do this manually, by setting LivePersistence = false, and handle the required events. And so you will need to keep a cached collection in memory (session/viewstate) and then set it to the dataSource.EntityCollection in the PerformSelect event.

This way you can save and refetch one entity in the PerformWork event and update the entityCollection in memory.