Ticket Q362121
Visible to All Users
Duplicate

How to provide an "Unsaved Changes Warning" in a Web application like in WinForms application ?

created 13 years ago

Hello,
In XAF Web applications, if you make a change in a detail view and leave the view (like clicking some other menu item in the navbar) without saving, you get no warning. My users prefer to be warned if they are leaving the form without saving the changes.
I see in the Main Demo Win version that this is implemented. Also, in Web applications if you click Delete you get a confirmation dialog on the client side.
How can I implement the following feature ?
If a user is leaving the edit view and there are changes in that view, a dialog should be presented to the user asking if he wants save the changes with the following options : Yes, No, Cancel.
This is quite important for my user acceptance tests and I will appreciate a solution very much.
Thanks,
Hakan.

Show previous comments (8)
Serge (DevExpress Support) 13 years ago

    Hi Hakan,
    I understand your position quite well. Let me clarify how we handle tickets in the Support Center. As you might have noticed, each type of tickets has a set of states. The state of this ticket was set to Duplicate. This means that it is devoted to a problem/case that has already been described in another ticket. We will not close the entire case - we will handle it in the context of another ticket (S32788 in your situation). At the same time, the Support Center allows you to close tickets or keep them active. If you wish to close a certain ticket, simply click the Close Ticket button.
    I hope this makes things clear. I think you also will be interested to know that we are working on major improvements on our support engine. It is a very complex task and many teams are involved; hence there are no separate suggestion tickets for the features we are currently developing. However, I can assure you that the results of this job will be widely announced. Please stay tuned to our website and announcements. You will not miss anything ;)
    Thanks,
    Serge

    Hakan Çelik (Helios) 13 years ago

      Hello Serge,
      Thank you for the clarification.
      Hakan

      Serge (DevExpress Support) 13 years ago

        You are very welcome, Hakan!

        Answers approved by DevExpress Support

        created 11 years ago (modified 11 years ago)

        Hello,
        Since this not neither a shared nor a specific to any module controller one can safely assume that is located in Xpand.ExpressApp.Web assembly under the SystemModules namespace https://github.com/expand/eXpand/blob/master/Xpand/Xpand.ExpressApp/Xpand.ExpressApp.Web/SystemModule/UnsavedObjectController.cs

          Comments (3)

            Found it, thanks!

              It looks like this controller does not work for popup windows - only root views. Particularly, QueryCanClose event does not fire for popup views, regardless of how it's getting closed: neither pressing Cancel nor clicking X on the popup window results in a callback. Is there a way to handle popup window closing as well?

                I'm interested in this, too, Andreas.  Currently we are using CollectionsEditMode = View so I think this will work but 14.2 defaults to CollectionsEditMode = Edit and we may be looking at using this mode for other apps as well.

                created 13 years ago (modified 12 years ago)

                Hello,
                After some pondering, I decided that having an interim solution would be much better than none. So, I come up with a temporary solution to the problem.
                According to my tests, it stops users from losing changes unintentionally. Admittedly not a very elegant solution on the UI side, but I will go with this one, until I will find time to implement something better. At least, it does it's job :)
                I've tried to cover all the cases and plug all the holes. Since I'm quite new to XAF, I will appreciate it very much, if you could take a look at the controller and comment on it.
                I really would like this to be as bullet proof as possible.
                Thanks,
                Hakan

                C#
                using System; using DevExpress.ExpressApp; using DevExpress.ExpressApp.Actions; using DevExpress.ExpressApp.CloneObject; using DevExpress.ExpressApp.Editors; using DevExpress.ExpressApp.SystemModule; using DevExpress.ExpressApp.Web.SystemModule; namespace IRM.Module.Web { [System.ComponentModel.DesignerCategory ("Code")] public partial class UnsavedObjectController : ViewController<DetailView> { private Boolean CanExitEditMode; private Boolean CancelActionTriggered; private Boolean ExitEditModeByCancel; private Boolean IsObjectSpaceModified; private Boolean IsWarningShown; private String ActionActiveID = "ActionActiveReason"; public UnsavedObjectController () { } protected override void OnActivated () { base.OnActivated (); CanExitEditMode = false; CancelActionTriggered = false; IsObjectSpaceModified = false; ExitEditModeByCancel = false; IsWarningShown = false; AdjustUIForMode (View.ViewEditMode); View.ViewEditModeChanged += View_ViewEditModeChanged; View.QueryCanClose += View_QueryCanClose; View.ObjectSpace.ObjectChanged += ObjectSpace_ObjectChanged; } protected override void OnDeactivated () { View.ObjectSpace.ObjectChanged -= ObjectSpace_ObjectChanged; View.QueryCanClose -= View_QueryCanClose; View.ViewEditModeChanged -= View_ViewEditModeChanged; AdjustUIForMode (ViewEditMode.View); base.OnDeactivated (); } void ObjectSpace_ObjectChanged (object sender, ObjectChangedEventArgs e) { if (!IsObjectSpaceModified) IsObjectSpaceModified = e.OldValue != e.NewValue; } void View_ViewEditModeChanged (object sender, EventArgs e) { AdjustUIForMode (View.ViewEditMode); } void View_QueryCanClose (object sender, System.ComponentModel.CancelEventArgs e) { e.Cancel = !IsExitEditModeAllowed (); } void HandleDetailActions (object sender, System.ComponentModel.CancelEventArgs e) { ActionBase anAction = sender as ActionBase; if (anAction.Id == "Cancel") { CancelActionTriggered = !CancelActionTriggered; CanExitEditMode = !CancelActionTriggered; ExitEditModeByCancel = true; e.Cancel = !IsExitEditModeAllowed (); } else { CancelActionTriggered = false; CanExitEditMode = true; ExitEditModeByCancel = false; } } protected Boolean IsExitEditModeAllowed () { if ((View.ViewEditMode == ViewEditMode.Edit) && !CanExitEditMode && IsObjectSpaceModified && !(IsWarningShown && ExitEditModeByCancel)) { String UnsavedObjectWarning; if (ExitEditModeByCancel) UnsavedObjectWarning = "Please, click Cancel again to cancel your changes."; else UnsavedObjectWarning = "You have unsaved changes. Please, Save or Cancel them."; DevExpress.ExpressApp.Web.ErrorHandling.Instance.SetPageError (new UserFriendlyException (UnsavedObjectWarning)); IsWarningShown = true; return false; } else return true; } protected void AdjustUIForMode (ViewEditMode EditMode) { if (EditMode == ViewEditMode.Edit) { Frame.GetController<WebDetailViewController> ().CancelAction.Executing += HandleDetailActions; Frame.GetController<WebDetailViewController> ().SaveAction.Executing += HandleDetailActions; Frame.GetController<WebDetailViewController> ().SaveAndCloseAction.Executing += HandleDetailActions; Frame.GetController<WebDetailViewController> ().SaveAndNewAction.Executing += HandleDetailActions; } else { Frame.GetController<WebDetailViewController> ().CancelAction.Executing -= HandleDetailActions; Frame.GetController<WebDetailViewController> ().SaveAction.Executing -= HandleDetailActions; Frame.GetController<WebDetailViewController> ().SaveAndCloseAction.Executing -= HandleDetailActions; Frame.GetController<WebDetailViewController> ().SaveAndNewAction.Executing -= HandleDetailActions; } Frame.GetController<WebRecordsNavigationController> ().Active [ActionActiveID] = EditMode == ViewEditMode.View; Frame.GetController<RefreshController> ().Active [ActionActiveID] = EditMode == ViewEditMode.View; Frame.GetController<WebNewObjectViewController> ().Active [ActionActiveID] = EditMode == ViewEditMode.View; Frame.GetController<WebDeleteObjectsViewController> ().Active [ActionActiveID] = EditMode == ViewEditMode.View; Frame.GetController<CloneObjectViewController> ().Active [ActionActiveID] = EditMode == ViewEditMode.View; } } }
                  Show previous comments (11)
                  Apostolis Bekiaris (DevExpress) 11 years ago

                    I added this controller in eXpandFramework core web module in version 13.1.6.19.

                      Apostolis, could you please comment on where this new controller can be found? What's the assembly file name and what's the class name? I tried googling but to no avail. Thank you.

                      Hakan Çelik (Helios) 11 years ago

                        Hello guys,
                        I was away from the forums for quite some time. I am very happy to see that a piece of code I've written 3 years ago is still useful to other people and been included in eXpandFramework.
                        Happy coding,
                        Hakan

                        Other Answers

                        created 10 years ago (modified 10 years ago)

                        Hakan, I have been playing with this today and I think this is definitely useful.  I took the liberty to add some tweaks.  Not fully tested yet but thought I'd post out here for anyone interested.

                        The 2 main changes are:

                        1. Incorporated the model interface so this can be set as an option and overridden at the view level.  (credit Xpand for this)
                        2. Get SaveAndNew to work

                        The issue with SaveAndNew is that it is made inactive by the NewObjectAction being inactive (also is disabled when the New Action is disabled)  See SaveAndNew

                        So I wound up adding a handler for the New Action.

                        Maybe we should add this to github?  I could see this getting better with other devs contributing.

                        Update: forgot to remove the handler I added in OnDeactivate:

                        C#
                        View.ObjectSpace.ObjectSaved -= ObjectSpace_ObjectSaved;
                        C#
                        using System; using DevExpress.Xpo; using DevExpress.ExpressApp; using DevExpress.ExpressApp.Actions; // using DevExpress.ExpressApp.CloneObject; using DevExpress.ExpressApp.Editors; using DevExpress.ExpressApp.SystemModule; using DevExpress.ExpressApp.Web.SystemModule; using System.ComponentModel; using DevExpress.ExpressApp.Model; namespace WCCJ.Module.Web { public interface IModelWarnForUnsavedChanges { [Category("Behavior")] bool WarnForUnsavedChanges { get; set; } } [ModelInterfaceImplementor(typeof(IModelWarnForUnsavedChanges), "Options")] public interface IModelClassWarnForUnsavedChanges : IModelWarnForUnsavedChanges { [Browsable(false)] [ModelValueCalculator("Application.Options")] IModelOptions Options { get; } } [ModelInterfaceImplementor(typeof(IModelWarnForUnsavedChanges), "ModelClass")] public interface IModelDetailViewWarnForUnsavedChanges : IModelWarnForUnsavedChanges { } [System.ComponentModel.DesignerCategory("Code")] public partial class UnsavedObjectController : ViewController<DetailView>, IModelExtender { private Boolean CanExitEditMode; private Boolean CancelActionTriggered; private Boolean SaveAndNewTriggered; private Boolean NewActionTriggered; private Boolean ExitEditModeByCancel; private Boolean IsObjectSpaceModified; private Boolean IsWarningShown; private String ActionActiveID = "ActionActiveReason"; public UnsavedObjectController() { } protected override void OnActivated() { base.OnActivated(); if (Frame is DevExpress.ExpressApp.Web.PopupWindow || !View.IsRoot) return; if (((IModelDetailViewWarnForUnsavedChanges)View.Model).WarnForUnsavedChanges) { CanExitEditMode = false; CancelActionTriggered = false; SaveAndNewTriggered = false; NewActionTriggered = false; IsObjectSpaceModified = false; ExitEditModeByCancel = false; IsWarningShown = false; AdjustUIForMode(View.ViewEditMode); View.ViewEditModeChanged += View_ViewEditModeChanged; View.QueryCanClose += View_QueryCanClose; View.ObjectSpace.ObjectChanged += ObjectSpace_ObjectChanged; View.ObjectSpace.ObjectSaved += ObjectSpace_ObjectSaved; } } void ObjectSpace_ObjectSaved(object sender, ObjectManipulatingEventArgs e) { IsObjectSpaceModified = false; } protected override void OnDeactivated() { base.OnDeactivated(); if (Frame is DevExpress.ExpressApp.Web.PopupWindow || !View.IsRoot) return; View.ObjectSpace.ObjectChanged -= ObjectSpace_ObjectChanged; View.QueryCanClose -= View_QueryCanClose; View.ViewEditModeChanged -= View_ViewEditModeChanged; AdjustUIForMode(ViewEditMode.View); } void ObjectSpace_ObjectChanged(object sender, ObjectChangedEventArgs e) { if (!IsObjectSpaceModified) IsObjectSpaceModified = (e.OldValue != e.NewValue) | (e.Object != View.CurrentObject); } void View_ViewEditModeChanged(object sender, EventArgs e) { AdjustUIForMode(View.ViewEditMode); } void View_QueryCanClose(object sender, System.ComponentModel.CancelEventArgs e) { e.Cancel = !IsExitEditModeAllowed(); } void HandleDetailActions(object sender, System.ComponentModel.CancelEventArgs e) { ActionBase anAction = sender as ActionBase; if (anAction.Id == "Cancel") { CancelActionTriggered = !CancelActionTriggered; CanExitEditMode = !CancelActionTriggered; ExitEditModeByCancel = true; e.Cancel = !IsExitEditModeAllowed(); } else if (anAction.Id == "New") { NewActionTriggered = !SaveAndNewTriggered; CanExitEditMode = !NewActionTriggered; SaveAndNewTriggered = false; e.Cancel = !IsExitEditModeAllowed(); } else if (anAction.Id == "SaveAndNew") { SaveAndNewTriggered = true; } else { SaveAndNewTriggered = false; CancelActionTriggered = false; NewActionTriggered = false; CanExitEditMode = true; ExitEditModeByCancel = false; } } protected Boolean IsExitEditModeAllowed() { if ((View.ViewEditMode == ViewEditMode.Edit) && !CanExitEditMode && IsObjectSpaceModified && !(IsWarningShown && ExitEditModeByCancel)) { String UnsavedObjectWarning; if (ExitEditModeByCancel) UnsavedObjectWarning = "Please click Cancel again to cancel your changes."; else UnsavedObjectWarning = "You have unsaved changes. Please Save or Cancel them."; DevExpress.ExpressApp.Web.ErrorHandling.Instance.SetPageError(new UserFriendlyException(UnsavedObjectWarning)); IsWarningShown = true; return false; } else return true; } protected void AdjustUIForMode(ViewEditMode EditMode) { if (EditMode == ViewEditMode.Edit) { Frame.GetController<WebModificationsController>().CancelAction.Executing += HandleDetailActions; Frame.GetController<WebModificationsController>().SaveAction.Executing += HandleDetailActions; Frame.GetController<WebModificationsController>().SaveAndCloseAction.Executing += HandleDetailActions; Frame.GetController<WebModificationsController>().SaveAndNewAction.Executing += HandleDetailActions; Frame.GetController<WebNewObjectViewController>().NewObjectAction.Executing += HandleDetailActions; } else { Frame.GetController<WebModificationsController>().CancelAction.Executing -= HandleDetailActions; Frame.GetController<WebModificationsController>().SaveAction.Executing -= HandleDetailActions; Frame.GetController<WebModificationsController>().SaveAndCloseAction.Executing -= HandleDetailActions; Frame.GetController<WebModificationsController>().SaveAndNewAction.Executing -= HandleDetailActions; Frame.GetController<WebNewObjectViewController>().NewObjectAction.Executing -= HandleDetailActions; } Frame.GetController<WebRecordsNavigationController>().Active[ActionActiveID] = EditMode == ViewEditMode.View; Frame.GetController<RefreshController>().Active[ActionActiveID] = EditMode == ViewEditMode.View; // don't inactivate this because then save and new is deactivated also; instead, handle the click action of the new like the CancelAction // Frame.GetController<WebNewObjectViewController>().Active[ActionActiveID] = EditMode == ViewEditMode.View; Frame.GetController<WebDeleteObjectsViewController>().Active[ActionActiveID] = EditMode == ViewEditMode.View; // Frame.GetController<CloneObjectViewController>().Active[ActionActiveID] = EditMode == ViewEditMode.View; } public void ExtendModelInterfaces(ModelInterfaceExtenders extenders) { extenders.Add<IModelOptions, IModelWarnForUnsavedChanges>(); extenders.Add<IModelClass, IModelClassWarnForUnsavedChanges>(); extenders.Add<IModelDetailView, IModelDetailViewWarnForUnsavedChanges>(); } } }
                          Show previous comments (5)
                          Dennis Garavsky (DevExpress) 10 years ago

                            @John: I see that you have already created a corresponding ticket in this regard: T272898. Feel free to make it public so that other XAF community members could benefit from the solution.

                              I know this solution not works for Next and Previous button. so the button being disabled. however. I do want make these two button works. any idea?
                              Thanks!
                              John

                              Dennis Garavsky (DevExpress) 9 years ago

                                @John: While I do not have a ready modification of the solution posted above and which handles Next/Previous buttons, I hope you can get it working as required after understanding that these commands are provided by the RecordsNavigationController class. You can handle the Executing/Execute and other events of its corresponding Actions to trigger custom logic. See eXpressApp Framework > Concepts > Extend Functionality > Built-in Controllers and Actions > Built-in Controllers and Actions in the System Module and eXpressApp Framework > Concepts > Extend Functionality > Customize Controllers and Actions  for more details.

                                Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

                                Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.