CslaActionExtender - useful enough vs. even more useful

CslaActionExtender - useful enough vs. even more useful

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


tiago posted on Tuesday, November 17, 2009

Hi Miguel Castro,

CslaActionExtender is very useful. It makes WinForms apps easier to write. In spite of all the trends, old and new - Web Forms, WPF, Silverlight and even Visual Web GUI - I find WinForms have a lot more to give and they provide the most productive developing environment.

As a side note, WPF is here for some time but it just doesn't seem to "take off". Silverlight is interesting enough for some applications (namely media apps) but I'm not sure if it has enough power to replace WinFoms applications in all fields. So let's make more investments on WinForms.

Back to the subject, I tried the sample ActionExtenderSample. I changed some settings, on OrderMaint.cs:

I added a standard ErrorProvider and on buttons:

I set DisableWhenClean property to True.

Take a break here so I can explain what I was expecting. "Intelligent buttons to see I expected" as old Yoda would put it.

What do I mean? Buttons that are enabled according to the object editing status: Save should be enabled only when a save can be done, Cancel should be enabled only there is something to cancel, etc. Take Save. The button should be disabled for:
a) unchanged objects (IsClean == True)
b) invalid objects (IsValid == False)

c) read only objects (CanEditObject == False)

That wasn't the case. I did the following test:

1. Object loads and only Close button is enabled - pass

2. Delete Miguel's name and leave the Card Holder field empty
2.1. Error icon shows on Card Holder field - pass
2.2. Only Cancel and Close buttons are enabled - fail

3. Fill in the Card Holder field
3.1. Error icon goes away - pass
3.2. All buttons are visible - pass

4. Again leave Card Holder field empty
2.1. Error icon shows on Card Holder field - pass
2.2. Only Cancel and Close buttons are enabled - fail

Step 4 was needed because some small changes to CslaActionExtender.cs made at first weren't enough to make this test pass.

Final result pass all tests.

Back to specifications above, a) and b) are ready but I din't test for c).

Changes made to CslaActionExtender.cs

I replaced

    private void InitializeControls(bool initialEnabling)
    {
      // controls will not be enabled until the BusinessObjectPropertyChanged event fires or if it's in an appropriate state now
      List<Control> extendedControls = new List<Control>();
      foreach (KeyValuePair<Control, CslaActionExtenderProperties> pair in _Sources)
      {
        if (pair.Value.ActionType != CslaFormAction.None)
        {
          Control ctl = pair.Key;
          if (initialEnabling)
          {
            ChangeEnabled(ctl, !pair.Value.DisableWhenClean);
            pair.Key.Click -= OnClick;
            pair.Key.Click += OnClick;
          }
          InitializeControl(ctl);
          extendedControls.Add(ctl);
        }
      }
    }

    private void InitializeControl(Control ctl)
    {
      if (!ctl.Enabled)
      {
        Csla.Core.ISavable businessObject = GetBusinessObject();
        if (businessObject != null)
        {
          Csla.Core.ITrackStatus trackableObject = businessObject as ITrackStatus;
          if (trackableObject != null)
            ChangeEnabled(ctl, trackableObject.IsNew || trackableObject.IsDirty || trackableObject.IsDeleted);
        }
      }
    }

with

    private void InitializeControls(bool initialEnabling)
    {
      // controls will not be enabled until the BusinessObjectPropertyChanged event fires or if it's in an appropriate state now
      List<Control> extendedControls = new List<Control>();
      foreach (KeyValuePair<Control, CslaActionExtenderProperties> pair in _Sources)
      {
        if (pair.Value.ActionType != CslaFormAction.None)
        {
          Control ctl = pair.Key;
          if (initialEnabling)
          {
            ChangeEnabled(ctl, !pair.Value.DisableWhenClean);
            pair.Key.Click -= OnClick;
            pair.Key.Click += OnClick;
          }
          InitializeControl(ctl, pair);
          extendedControls.Add(ctl);
        }
      }
    }

    private void InitializeControl(Control ctl, KeyValuePair<Control, CslaActionExtenderProperties> pair)
    {
      if (pair.Value.DisableWhenClean)
      {
        Csla.Core.ISavable businessObject = GetBusinessObject();
        if (businessObject != null)
        {
          Csla.Core.ITrackStatus trackableObject = businessObject as ITrackStatus;
          if (trackableObject != null)
          {
            if (pair.Value.ActionType == CslaFormAction.Cancel)
              ChangeEnabled(ctl, trackableObject.IsNew || trackableObject.IsDirty || trackableObject.IsDeleted);
            if (pair.Value.ActionType == CslaFormAction.Save)
              ChangeEnabled(ctl, (trackableObject.IsNew || trackableObject.IsDirty || trackableObject.IsDeleted)
                && trackableObject.IsValid);
          }
        }
      }
    }

That's it!

Of course there could be another property to say DisableWhenUnsavable or something of the sort. I had this in some stage but removed it later on. Why do we need to say twice that we want the button disabled when the action it is supposed to do can't be done? Let's just say DisableWhenClean means DisableWhenUseless.

 

Miguel,

can you consider adding these changes to Csla 3.8.x trunk?

Thanks.

 

Jonny,

in case these changes aren't going into Csla trunk, you can use them for MyCsla.

 

PS 1 - I didn't test for child dirtiness.

PS 2 - I enclose the changed file for Csla 3.8.0.

tiago replied on Tuesday, November 17, 2009

tiago:

PS 2 - I enclose the changed file for Csla 3.8.0.

Well in fact for Csla 3.8.1

RockfordLhotka replied on Saturday, November 21, 2009

I'll add this to the wish list.

tiago replied on Sunday, November 22, 2009

RockfordLhotka:
I'll add this to the wish list.

It's not that simple. I mean the solution.

Suppose you have a form with no ErrorProvider in it (the sponsor doesn't like red circles or something). You seat at a computer that is displaying that form and only Cancel and Close are enabled. All the Save, Save/Close, Save/New buttons are disabled. You say to yourself:

"Hmmm... I guess there is something fishy about this data: if the form can't save but can cancel, that's the only explanation. Let me guess what field isn't valid..."

And then you add:

"Pitty they didn't put a validate button!"

In fact all the code for the validate button is there. My second version adds a new ActionType "Validate" that displays the same alert box that was shown on save when the form isn't valid.

The problem I have is the default ObjectIsValid message that shows if the form is valid. In order to have a default, I need to add it to Csla resources.

In order to avoid breaking changes (in code and in behaviour) I added a new property SetDisableWhenUseless and kept the SetDisableWhenClean property marking it obsolete and not showing it in the designer.

Existing code behaves the same way. When designing a new form, you only have the design option of usign SetDisableWhenUseless. If you set both properties, the button will ignore the obsolete property and behave the new way.

Hiding the property SetDisableWhenClean in the designer is questionable.

 

tiago replied on Sunday, April 15, 2012

To cut a long story short, I attach a ZIP file containing 3 files:

They build under Csla 4.3.10 and they go into \Source\Csla.Windows

JonnyBee replied on Sunday, April 15, 2012

Hi Tiago,

This code is not "bulletproof":

              case CslaFormAction.Validate:

                if (savableObject is Csla.Core.BusinessBase)
                {
                  Csla.Core.BusinessBase businessObject = savableObject as Csla.Core.BusinessBase;
                  if (businessObject.BrokenRulesCollection.Count > 0)
                    MessageBox.Show(businessObject.BrokenRulesCollection.ToString(), Resources.Error, MessageBoxButtons.OK, MessageBoxIcon.Error);
                  else
                      MessageBox.Show(ObjectIsValidMessage, Resources.Information, MessageBoxButtons.OK, MessageBoxIcon.Information);
                }

                break;

There's nothing that prevents anyone from using a BusinessBase object that has children so the code should check for <bo>.IsValid

Another known bug is the OnClick

¨                 if (objectValid)
                  {
                    CslaActionCancelEventArgs savingArgs = new CslaActionCancelEventArgs(
                      false, props.CommandName);
                    OnObjectSaving(savingArgs);

                    if (!savingArgs.Cancel) // this line was !args.Cancel

And there's a few other know bugs to - check in bugtracker.

tiago replied on Sunday, April 15, 2012

JonnyBee

Hi Tiago,

This code is not "bulletproof":

(...)

And there's a few other know bugs to - check in bugtracker.

Found 4 open issues. There are 3 bugs

http://www.lhotka.net/cslabugs/edit_bug.aspx?id=426

http://www.lhotka.net/cslabugs/edit_bug.aspx?id=700

http://www.lhotka.net/cslabugs/edit_bug.aspx?id=843

and 1 enhancement (an earlier version of this one)

http://www.lhotka.net/cslabugs/edit_bug.aspx?id=650

Attached is the updated files with solution for issues 426 and 843. Issue 700 duplicates 426. Anyway I tested the sample and it passed.

Notice that 3 new Csla resources are needed but those lines are commented and hard coded strings are used instead.

cwinkelmann replied on Monday, April 16, 2012

What is the status of CslaActionExtender in Csla 4.3.10? So at this point all known bugs should be solved with this v3 set of files for 4.3.10?

I didn't find any open bugs listed here: http://www.lhotka.net/cslabugs/search.aspx

but I may not be using the search function correctly.

RockfordLhotka replied on Monday, April 16, 2012

Nothing has changed in Csla.Windows (Windows Forms support) for a very long time. This hasn't been a priority - our limited dev resources have been busy just keeping up with all the new technologies...

I have at least one volunteer who's in the process of signing a contributor agreement, and who has indicated interest in working on some of the Windows Forms items in bugtracker, so there may be some changes that'll come in 4.5.

tiago replied on Monday, April 16, 2012

Hi Rocky,

The code I post is always public domain. If I need to sign a contributor agreement in order for Csla to use the code above, just tell me where the dotted line is.

RockfordLhotka

I have at least one volunteer who's in the process of signing a contributor agreement, and who has indicated interest in working on some of the Windows Forms items in bugtracker, so there may be some changes that'll come in 4.5.

tiago replied on Sunday, April 29, 2012

Hi all,

Found another issue that was present on Miguel's code and that I kept in the changed code.

1) Set DisableWhenClean (or the new DisableWhenUseless) property to True on buttons:

2) Load a form => buttons are disabled (OK)

3) Change something => buttons are enabled (OK)

4) Save the form  => buttons are disabled (OK)

5) Again, change something => buttons are disabled (NOK)

Note - DisableWhenUseless means the button is disabled when no action is available for it (Save for the save action family and Undo for the cancel action family).

 

cwinkelmann replied on Tuesday, May 01, 2012

That is exactly the same result I have on a Demo project. I was wondering if I was not performing the save operations correctly and that would cause some odd behavior with the child/parent change notification, but I thought everything I did was run of the mill type stuff.

So I am toying with the root, child, grandchild pattern and after having saved the root, which triggers the Child_Update on child and grandchild, then the child changed events don't seem to fire when I modify the child or grandchild BO. I'm not sure if there is a problem with the clone process that it is not creating the same even handler subscriptions or if there is a problem somewhere else. I'm not entirely sure it is a problem with ActionExtender as my root object is not listening to child changed events after having saved once...

I've attached a proof of concept hoping that will help anyone who wants to try Tiago's latest version of the ActionExtender.

 

JonnyBee replied on Sunday, May 06, 2012

@cwinkelmann

I looked at your code and the problem is most likely your code and how you attach to the events. You should not do this in the constructor.

Alt 1: Do not attach to the events - simply override OnChildChanged event like this in EditableRoot.cs. This relies on CSLA to use the AddXYEventHook methods to hook into events properly and your code just override the method OnChildChanged (that in turn triggers the ChildChanged event).

    protected override void OnChildChanged(Csla.Core.ChildChangedEventArgs e)
    {
      base.OnChildChanged(e);
      Calculate();
    }

Alt 2:Override the proper methods to Add/Remove event hooks like this:

 

    private EditableRoot()
    { 
        /* Force use of Object Factory methods */
}
    protected override void OnRemoveEventHooks(Csla.Core.IBusinessObject child)
    {
      base.OnRemoveEventHooks(child);
      this.ChildChanged -= EditableRoot_ChildChanged;
    }
 
    protected override void OnAddEventHooks(Csla.Core.IBusinessObject child)
    {
      base.OnAddEventHooks(child);
      this.ChildChanged += EditableRoot_ChildChanged;
    }

private void EditableRoot_ChildChanged(object sender, Csla.Core.ChildChangedEventArgs e) { Calculate(); }

The OnXYEventHooks method is also used by CSLA to attach itself to the events on the child objects.

 

cwinkelmann replied on Monday, May 07, 2012

@JonnyBee: That worked perfectly.

I added both ways to the demo project and I'll continue developing it. I hope to add functionality to it as a demo of lots of CSLA functionality, so I'll eventually propose adding it to the CSLAContrib project.

tiago replied on Monday, May 07, 2012

Hi Jonny,

This works perfectly. I enclose V4 with the corrected code for IButtonControl (button and LinkLabel) and for ToolStripButton. Thanks.

This isn't closed as the broken rules (and validation) message box is far from ready:

The later depends on GetPropertyFriendlyName proposal on http://forums.lhotka.net/forums/p/3424/52654.aspx#52654

Regards

JonnyBee replied on Tuesday, May 08, 2012

Hi,

You may also want to look into BusinessRules.GetAllBrokenRules().

This static method will return a list of all broken rules in <bo> and all child/grandchild objects and may be presented as a list or a tree view.

tiago replied on Wednesday, May 09, 2012

JonnyBee

Hi,

You may also want to look into BusinessRules.GetAllBrokenRules().

This static method will return a list of all broken rules in <bo> and all child/grandchild objects and may be presented as a list or a tree view.

I did have a look and found it not very straightforward to use. So I left this issue to later on.

On the mean time I found that this controls force all buttons to enabled. This isn't a good idea because I might need to "bind" the enabled state to a BO or Ui state. Another feature to add...

<edit>

It's fixed.

</edit>

tiago replied on Monday, May 14, 2012

Hi all,

The attached code solves the issue

  • on initialize, only buttons with DisableWhenUseless or DisableWhenClean options should be forced to enabled state
  • This is important if you use CslaActionExtenderTsb (for ToolStripButton) since you can have navigation buttons that are enabled by other UI events. Of course you can also have navigation buttons that are plain buttons, so the fix applies to both classes.

    Regards

    tiago replied on Saturday, May 05, 2012

    Hi all,

    No progress on this front. After several hours of debug, I just couldn't find the soltuion for the "button is always disabled after a save" issue.

    JonnyBee replied on Sunday, May 06, 2012

    @Tiago:

    First I'd refactor so that the ActionExtender has AddEventHoks and RemoveEventHooks methods:

        public void ResetActionBehaviors(ISavable objectToBind)
        {
          InitializeControls(true);
     
          BindingSource rootSource = _dataSource as BindingSource;
     
          if (rootSource != null)
          {
            AddEventHooks(objectToBind);
          }
          
          _bindingSourceTree = BindingSourceHelper.InitializeBindingSourceTree(_container, rootSource);
          _bindingSourceTree.Bind(objectToBind);
     
        }
     
        private void AddEventHooks(ISavable objectToBind)
        {
          // make sure to not attach many times
          RemoveEventHooks(objectToBind);
     
          INotifyPropertyChanged propChangedObjParent = objectToBind as INotifyPropertyChanged;
          if (propChangedObjParent != null)
          {
            propChangedObjParent.PropertyChanged += propChangedObj_PropertyChanged;
          }
     
          INotifyChildChanged propChangedObjChild = objectToBind as INotifyChildChanged;
          if (propChangedObjChild != null)
          {
            propChangedObjChild.ChildChanged += propChangedObj_ChildChanged;
          }
        }
     
        private void RemoveEventHooks(ISavable objectToBind)
        {
          INotifyPropertyChanged propChangedObjParent = objectToBind as INotifyPropertyChanged;
          if (propChangedObjParent != null)
          {
            propChangedObjParent.PropertyChanged -= propChangedObj_PropertyChanged;
          }
     
          INotifyChildChanged propChangedObjChild = objectToBind as INotifyChildChanged;
          if (propChangedObjChild != null)
          {
            propChangedObjChild.ChildChanged -= propChangedObj_ChildChanged;
          }
        }

    And then change the save code to make sure to attach events:

                            try
                            {
     
                              RemoveEventHooks(savableObject);
                              savableObject = savableObject.Save() as Csla.Core.ISavable;
     
                              OnObjectSaved(new CslaActionEventArgs(props.CommandName));
     
                              switch (props.PostSaveAction)
                              {
                                case PostSaveActionType.None:
     
                                  if (source != null && props.RebindAfterSave)
    {                                 _bindingSourceTree.Bind(savableObject);                                  AddEventHooks(savableObject);
    }
                                  break;                             case PostSaveActionType.AndClose:                               CloseForm();                               break;                             case PostSaveActionType.AndNew:                               OnSetForNew(new CslaActionEventArgs(props.CommandName));
                              AddEventHooks(savableObject);                               break;                           }                         }                         catch (Exception ex)                         {                           _bindingSourceTree.Bind(savableObject); //Issue ID:  426                           AddEventHooks(savableObject);                           OnErrorEncountered(new ErrorEncounteredEventArgs(props.CommandName, new ObjectSaveException(ex)));                           raiseClicked = false;                         }

    Copyright (c) Marimer LLC