CSLA .NET

Vibrant discussion about CSLA .NET and using the framework to build great business applications.

CslaActionExtender - useful enough vs. even more useful

rated by 0 users
This post has 19 Replies | 4 Followers

Top 25 Contributor
Posts 461

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:

  • Save
  • Save/New
  • Save/Close
  • Cancel

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 Freitas Leal, CslaGenFork (Open Source CSLA code generator)

Top 25 Contributor
Posts 461
tiago:

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

Well in fact for Csla 3.8.1

Tiago Freitas Leal, CslaGenFork (Open Source CSLA code generator)

Top 10 Contributor
Posts 9,282
I'll add this to the wish list.

Rocky

Top 25 Contributor
Posts 461

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 Freitas Leal, CslaGenFork (Open Source CSLA code generator)

Top 25 Contributor
Posts 461

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

  • CslaActionExtender.cs
  • CslaActionExtenderProperties.cs
  • Enums.cs

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

Tiago Freitas Leal, CslaGenFork (Open Source CSLA code generator)

Top 10 Contributor
Posts 1,815
JonnyBee replied on Sun, Apr 15 2012 3:25 PM

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.

Jonny Bekkum, Norway CslaContrib Coordinator

Top 25 Contributor
Posts 461

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.

Tiago Freitas Leal, CslaGenFork (Open Source CSLA code generator)

Top 500 Contributor
Posts 24

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.

Top 10 Contributor
Posts 9,282

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.

Rocky

Top 25 Contributor
Posts 461

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 Freitas Leal, CslaGenFork (Open Source CSLA code generator)

Top 25 Contributor
Posts 461

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:

  • Save
  • Save/New
  • Save/Close
  • Cancel

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).

 

Tiago Freitas Leal, CslaGenFork (Open Source CSLA code generator)

Top 500 Contributor
Posts 24

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.

 

Top 25 Contributor
Posts 461

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.

Tiago Freitas Leal, CslaGenFork (Open Source CSLA code generator)

Top 10 Contributor
Posts 1,815
JonnyBee replied on Sun, May 6 2012 12:07 AM

@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.

 

Jonny Bekkum, Norway CslaContrib Coordinator

Top 10 Contributor
Posts 1,815
JonnyBee replied on Sun, May 6 2012 12:36 AM

@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;                         }

Jonny Bekkum, Norway CslaContrib Coordinator

Page 1 of 2 (20 items) 1 2 Next > | RSS

Copyright (c) 2006-2010 Marimer LLC. All rights reserved.
Email admin@lhotka.net for support.
Powered by Community Server (Non-Commercial Edition), by Telligent Systems