Any Issues With Lists And Async Rules? (Silverlight)

Any Issues With Lists And Async Rules? (Silverlight)

Old forum URL: forums.lhotka.net/forums/t/10820.aspx


MadGerbil posted on Thursday, October 27, 2011

I've a BusinessListBase that is generated from data that is unreliable so I check all the rules and quite often many of them are broken.  When all the rules in the collection are no longer broken I want the "Save" button to become enabled.  The "Save" button is bound to CanSave which is set in response to ChildChanged, BusyChanged, etc - just like the ViewModel<T>.  Out of all the rules, there is only one that is asychronous.

The rule works as expected in that the values are validated against the datastore and the single object can become valid but even when all of the objects in the collection are valid the list is not savable.   IsValid and IsDirty are all True - it's just that IsBusy is also true and that makes IsSavable to be false.

The thing is, sometimes it works if the list of items is very small (4 items) but it has never worked if the list of items is very large.  

When the list is being loaded up on the server the rule is run using a command object and I wonder if it's possible that the object is getting serialized and send across the wire in a busy state with no way to remove that busy state once it's deserialized on the client.  I dunno...I'm only guessing at this point.  I don't know how the object gets stuck in a busy state while it is completely valid.  Any clues?

RockfordLhotka replied on Thursday, October 27, 2011

You absolutely should not run async rules on the server. There is nothing that stops the graph from being serialized to the client before the rule completes, unless you write such code (like manual locking at the bottom of DataPortal_Fetch).

The best solution (imo) is to make your rule smart enough to be async on the client, and sync on the server. This is done using ApplicationContext.ExecutionLocation, and there's an example (iirc) in the Using CSLA 4: Creating Business Objects ebook.

MadGerbil replied on Friday, October 28, 2011

I don't believe that I am; however, I'm having issues so something isn't right.

Here is my rule:

        class IsValidPCRCode : Csla.Rules.BusinessRule
        {
            public Csla.Core.IPropertyInfo IsImportProperty { get; set; }

            public IsValidPCRCode(Csla.Core.IPropertyInfo primaryProperty, Csla.Core.IPropertyInfo isImportProperty)
                : base(primaryProperty)
            {
                IsAsync = (Csla.ApplicationContext.ExecutionLocation != ApplicationContext.ExecutionLocations.Server);
                IsImportProperty = isImportProperty;
                InputProperties = new List<Csla.Core.IPropertyInfo>();
                InputProperties.Add(primaryProperty);
                InputProperties.Add(isImportProperty);
            }

            protected override void Execute(Csla.Rules.RuleContext context)
            {
                var code = (string)context.InputPropertyValues[PrimaryProperty];
                var isImport = (bool)context.InputPropertyValues[IsImportProperty];

                if (IsAsync)
                {
                    ValidatePCRCodeCommand cmd = new ValidatePCRCodeCommand(code, isImport);
                    DataPortal<ValidatePCRCodeCommand> dpCmd = new DataPortal<ValidatePCRCodeCommand>();
                    dpCmd.ExecuteCompleted += (o, e) =>
                    {
                        if (e.Error != null)
                            context.AddErrorResult(e.Error.Message);
                        else if (!e.Object.IsValid)
                            context.AddErrorResult("The CR Code is invalid.");
                        context.Complete();
                    };

                    dpCmd.BeginExecute(cmd);
                }
                else
                {
#if !SILVERLIGHT
                    ValidatePCRCodeCommand cmd = new ValidatePCRCodeCommand(code, isImport);
                    cmd = DataPortal.Execute<ValidatePCRCodeCommand>(cmd);

                    if (!cmd.IsValid)
                        context.AddErrorResult("The CR Code is invalid.");
#endif
                }
            }
        }

1: The rule works in that the expect results are returned.  The rule indicates that the objet is invalid/valid in an expected way on both the server and the client.

2: I've a symbol that appears next to each line in the datagrid.  If the line item is valid the symbol is green and if the line item is invalid the symbol is red.  If I put a correct value into the field the symbol turns from red to green as expected.  Switching the values from invalid to valid and back and forth gives all the expected results.

3: When I examine the collection item when all line items have been corrected the collection IsValid=True, IsDirty=True, IsSavable=False, IsBusy=True.  In my other collections where there are no asynchronous rules the whole thing operates as expected so there is something about the asynchronous that seems to keep my object busy even after the collection is valid.

I've this code to wire the model to events:

    if (Model != null)
            {
                var npc = Model as INotifyPropertyChanged;
                if (npc != null)
                    npc.PropertyChanged -= Model_PropertyChanged;
                var ncc = Model as INotifyChildChanged;
                if (ncc != null)
                    ncc.ChildChanged -= Model_ChildChanged;
                var nb = Model as INotifyBusy;
                if (nb != null)
                    nb.BusyChanged -= Model_BusyChanged;
                var cc = Model as INotifyCollectionChanged;
                if (cc != null)
                    cc.CollectionChanged -= Model_CollectionChanged;
                var npc2 = Model as INotifyPropertyChanged;
                if (npc2 != null)
                    npc2.PropertyChanged += Model_PropertyChanged;
                var ncc2 = Model as INotifyChildChanged;
                if (ncc2 != null)
                    ncc2.ChildChanged += Model_ChildChanged;
                var nb2 = Model as INotifyBusy;
                if (nb2 != null)
                    nb2.BusyChanged += Model_BusyChanged;
                var cc2 = Model as INotifyCollectionChanged;
                if (cc2 != null)
                    cc2.CollectionChanged += Model_CollectionChanged;
           }

         private void Model_BusyChanged(object sender, BusyChangedEventArgs e)
        {
             //only set busy state for entire object.  Ignore busy state based
             //on asynch rules being active
            if (e.PropertyName == string.Empty)
                IsBusy = e.Busy;
            else
                OnSetProperties();
        }

        private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            OnSetProperties();
        }

        private void Model_ChildChanged(object sender, ChildChangedEventArgs e)
        {
            OnSetProperties();
        }

        private void Model_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            OnSetProperties();
        }

        protected virtual void OnSetProperties()
        {
            SetProperties();
        }

        private void SetProperties()
        {
            ITrackStatus targetObject = Model as ITrackStatus;
            INotifyBusy busyObject = Model as INotifyBusy;
            bool isObjectBusy = false;
            if (busyObject!=null && busyObject.IsBusy)
                isObjectBusy = true;

            if (targetObject != null)
            {
                   CanSave = targetObject.IsSavable;
                   Debug.WriteLine("***************");
                   Debug.WriteLine("Is Dirty: " + targetObject.IsDirty);
                   Debug.WriteLine("Is Valid: " + targetObject.IsValid);
                   Debug.WriteLine("Is Savable: " + targetObject.IsSavable);
                   Debug.WriteLine("Is Busy: " + isObjectBusy);
            }
        }

 

As you can see I write out the object state to the debugger window.   The IsDirty and IsValid values change as expected as I make changes to individual objects in the collection - the entire collection (Model) doesn't show itself as valid until all the corrections have been made; however, the object stays busy long after the UI has indicated that the rule has been checked and found to be not broken.   Even if the model is valid, based on the result of an asynchronous rule, the Isvalid property updates but IsBusy remains true thereby making it IsSavable false.    So there is something I'm missing about IsBusy here.

MadGerbil replied on Friday, October 28, 2011

Sometimes it will work on a short list (4 items) but it never works on a large list.

RockfordLhotka replied on Friday, October 28, 2011

What version of CSLA?

MadGerbil replied on Friday, October 28, 2011

Version 4.2.0 Beta

Would it matter if my Model in this case was a child collection?  I use the UOW model to load up my collection and some supporting objects (support objects are read only) so my Model is bound to that child collection of the UOW object.

MadGerbil replied on Friday, October 28, 2011

For this object(s) my output window gets lots of thread exits:

 The thread '<No Name>' (0x1490) has exited with code 0 (0x0).
The thread '<No Name>' (0x1060) has exited with code 0 (0x0).
The thread '<No Name>' (0x1178) has exited with code 0 (0x0).
The thread '<No Name>' (0x9d0) has exited with code 0 (0x0).
The thread '<No Name>' (0x1030) has exited with code 0 (0x0).
The thread '<No Name>' (0x1044) has exited with code 0 (0x0).
The thread '<No Name>' (0x1034) has exited with code 0 (0x0).
The thread '<No Name>' (0x9d8) has exited with code 0 (0x0).
The thread '<No Name>' (0x1040) has exited with code 0 (0x0).
The thread '<No Name>' (0x103c) has exited with code 0 (0x0).
The thread '<No Name>' (0x62c) has exited with code 0 (0x0).
The thread '<No Name>' (0xe64) has exited with code 0 (0x0).
The thread '<No Name>' (0x1038) has exited with code 0 (0x0).
The thread '<No Name>' (0xbec) has exited with code 0 (0x0).
The thread '<No Name>' (0xc5c) has exited with code 0 (0x0).
The thread '<No Name>' (0x10f4) has exited with code 0 (0x0).

I wonder if I'm spawning tons of threads somehow and reaching a thread limit.

MadGerbil replied on Friday, October 28, 2011

I'm going to highlight two lines below - each of which writes to the output pane.

 

       protected virtual void BindChangedEvents()
        {
            if (Model != null)
            {
                var npc = Model as INotifyPropertyChanged;
                if (npc != null)
                    npc.PropertyChanged -= Model_PropertyChanged;
                var ncc = Model as INotifyChildChanged;
                if (ncc != null)
                    ncc.ChildChanged -= Model_ChildChanged;
                var nb = Model as INotifyBusy;
                if (nb != null)
                    nb.BusyChanged -= Model_BusyChanged;
                var cc = Model as INotifyCollectionChanged;
                if (cc != null)
                    cc.CollectionChanged -= Model_CollectionChanged;
                var npc2 = Model as INotifyPropertyChanged;
                if (npc2 != null)
                    npc2.PropertyChanged += Model_PropertyChanged;
                var ncc2 = Model as INotifyChildChanged;
                if (ncc2 != null)
                    ncc2.ChildChanged += Model_ChildChanged;
                var nb2 = Model as INotifyBusy;
                if (nb2 != null)
                    nb2.BusyChanged += Model_BusyChanged;
                var cc2 = Model as INotifyCollectionChanged;
                if (cc2 != null)
                    cc2.CollectionChanged += Model_CollectionChanged;
           }
        }

        private void Model_BusyChanged(object sender, BusyChangedEventArgs e)
        {
             //only set busy state for entire object.  Ignore busy state based
             //on asynch rules being active

            Debug.WriteLine("Model_BusyChanged: " + e.Busy);
           
            //if (e.PropertyName == string.Empty)
            //    IsBusy = e.Busy;
            //else
                OnSetProperties();
        }

        private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            OnSetProperties();
        }

        private void Model_ChildChanged(object sender, ChildChangedEventArgs e)
        {
            OnSetProperties();
        }

        private void Model_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            OnSetProperties();
        }

        protected virtual void OnSetProperties()
        {
            SetProperties();
        }

        private void SetProperties()
        {
            ITrackStatus targetObject = Model as ITrackStatus;
            INotifyBusy busyObject = Model as INotifyBusy;

            bool isObjectBusy = false;
            if (busyObject!=null && busyObject.IsBusy)
                isObjectBusy = true;

            if (targetObject != null)
            {
                   CanSave = targetObject.IsSavable;
                   Debug.WriteLine("***************");
                   //Debug.WriteLine("Is Dirty: " + targetObject.IsDirty);
                   //Debug.WriteLine("Is Valid: " + targetObject.IsValid);
                   //Debug.WriteLine("Is Savable: " + targetObject.IsSavable);
                   Debug.WriteLine("Set Properties: " + isObjectBusy);
            }
        }

First, I've wired up the Model property to the root UOW object.  What I don't understand is why the value for 'Busy' in the Model_BusyChanged is FALSE but when SetProperties is called right afterwards the value for Model.IsBusy is set to TRUE.  

So we have a debug line that is telling me the IsBusy property has changed and is now FALSE but the root object still has an IsBusy value of TRUE.   Would it be wrong to expect these two values to be the same at all times?

JonnyBee replied on Sunday, October 30, 2011

Hi,

There is 3 new properties on rules in Csla 4.2 that can restrict when a rule is allowed to run.
I assume your rules inherit from CommonRules.CommonBusinessRule?

For async rules I would only allow them to run when either a field is changed and possibly when CheckRules is called.
I would not allow async rules to execute in context as AffectedProperty as that may trigger the async rule multiple times (in parallell).  This can be controlled by:

So my recommended setting would be:

CanRunAsAffectedProperty = false; // expensive rule - do not run as affected property or as inpout property is changed 
CanRunInCheckRules = !IsAsync; // change to true if rule can run in CheckRules in client side 
CanRunOnServer = !IsAsync;     // Change to true if rule can run i DataAccess (logical server side of dataportal) 

MadGerbil replied on Monday, October 31, 2011

Thank you for the help.

I'm able to give my users a very nice experience thanks to the help that I get here.   I'd had users tell me that they'd rather use my applications over those costing hundreds of thousands of dollars because they like having custom software that just works and my ability to deliver that has been in large part due to help I've received here.

Copyright (c) Marimer LLC