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.
We have closed this ticket because another page addresses its subject:
UI.Web - Show a confirmation dialog when a user leaves a page with modified data or changes the current objectHow to provide an "Unsaved Changes Warning" in a Web application like in WinForms application ?
Answers approved by DevExpress Support
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
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.
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;
}
}
}
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.
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
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:
- Incorporated the model interface so this can be set as an option and overridden at the view level. (credit Xpand for this)
- 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>();
}
}
}
@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
@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.
Hello Hakan,
Thank you for contacting us. I am afraid we do not have an easy solution for this task at the moment. It requires some changes in the core of our framework. We will be happy to implement this behavior by default in our framework when working on the SystemModules.Save.Web - Ask for confirmation when the user leaves the list or details form with the modified object, or changes the current object of this View suggestion.
Thanks,
Dennis
Hello Dennis,
I can understand your situation perfectly well, from a software provider point of view, since I'm one for about 18 years.
On the other hand, suggestion S32788 has been waiting in "Processed (Accepted - Release TBD)" state since 7/7/2009 10:15:12 AM .
Yes, surely there are a lot of more pressing issues and things in your queues, but that's an awfully long to time to implement this, considering today is 12/26/2011, don't you think ?
If I decide to not wait and implement this myself, would there be some leads and suggestions you can provide me ?
Thanks,
Hakan.
Hello Hakan,
Thank you for your feedback. As I mentioned earlier, some changes in the core of our framework are required in order to implement this feature. For instance, the ObjectSpace.ModifiedChanged event is not raised when you are moving from an editable DetailView with some changes to another View. In addition, there is no nice way to display a popup window on the Web (Web - Support complex dialogs on the client-side in XAF ASP.NET applications (similar to the MessageBox in Windows Forms)).
So, it would not be easy to solve this task at the moment, unless you decide to provide another way of detecting whether an editable View has some uncommitted changes.
Let me know if I can assist you further.
Thanks,
Dennis
P.S.
In 2012 we plan to devote a big part of our time to improving both developer and end-user usability, and I hope this particular issue will be addressed. I cannot promise it though, as always.
Hello Dennis,
Thank you for the detailed answer. Let me give my response two parts.
Also, I've already checked most of the links provided in S33494 before. I'm quite intent on doing my homework as well as I can, before asking a question.
<digression (No offense intended in this part, meant to be funny, but it is real>
Please, imagine that you are in my shoes and consider the following very real scenario, which I'm about to face in the first week of new year.
We are in a demonstration meeting with the CTO of a blue chip company (a multinational one), with a potential of at least 50 user licenses for my company. I'm demonstrating the abilities of our software, while the sales manager doing his sales pitch etc. Everything's going well.
Then, the CTO decides to give it a ride, we give him a test account, shake hands all around and leave. Since the guy is very much worth his salt, next day he calls us to his office and we have the following conversation :
CTO : Mr. Celik, I've edited this record and inadvertently clicked a navigation item, my changes are not saved and I didn't get any warning. This may cause a lot of data loss for our users. I checked this a few times before asking you, but is there a bug in here ?
Hakan : Mr. CTO, I'm awfully sorry to inform you that, currently we don't have the ability yo warn you before your edits are lost. But, I assure you, we are working very hard to provide the feature as soon possible (Inside voice : You lying [censored]). In the meantime, please tell your users to remember clicking save before they leave a view. (Smiles anxiously, Inside voice : Oh boy, here we go !)
CTO : (Inside voice : What the…?) Mr. Celik I hope, I'm getting you wrong. You are going to charge us a pretty hefty amount of dollars for this system, and a yearly maintenance fee. Are you really trying to tell me that, you can not even warn users of unsaved changes, while every nontrivial piece of software on Earth does that on default, including [censored] Windows Notepad ?
Hakan : Well, oh, errm (Inside voice : What the [censored] am I going to do now ?)
I realize that this might sound like pretty exaggerated (it is not) and also sound like one of the stories of BOFH http://www.theregister.co.uk/odds/bofh/ , but what can I say, I'm a fan :)
</digression>
On a much more serious note, this is not a cosmetic feature, it is a very basic usability feature virtually every computer user (including you and me) have come to rely on. Please consider, if you were to use a version of Visual Studio or Office application without this feature, how much work would you lose inadvertently ?
Dennis, I'm not letting this go. I'm perfectly willing to go the extra mile, to implement whatever is necessary to have this feature, but I need you guidance here.
Thanks,
Hakan.
Hello Hakan,
Thank you for your invaluable feedback. I fully understand your situation and I also find this functionality helpful. That is why we plan to implement it in the future.
However, as I mentioned, the above framework simply does not have anything ready for your use at the moment. Here, I mean any pure XAF features of course, like a property in the model and so on.
The good news is that it is still possible to implement the same feature as in other regular ASP.NET applications. In other words, you can track changes made on the client's browser, set some flag and then display a message box again on the client if this flag is dirty. Refer to the http://stackoverflow.com/questions/1119289/how-to-show-the-are-you-sure-you-want-to-navigate-away-from-this-page-when-ch help article for more information.
You may probably find other ways of accomplishing this as, after all, this functionality is not specific to XAF applications.
To track changes on the client, you can access our ASPx editors and handle their events, e.g the ASPxClientEdit.ValueChanged client side event. How to use our ASPx client side events is described in the documentation for our ASP.NET controls. You are also welcome to contact our ASP.NET team via the Support Center if you experience any difficulties with their editors.
How do you access these ASPx editors in XAF and assign some client side functionality for them at runtime? We have a help article showing how it can be done: Access Editor Settings
If you look in the Support Center, you can also find ready code examples on this.
Please let me know if we can assist you further. And, please keep us informed of your progress and it is not less important for us than for you. Thank you in advance!
Thanks,
Dennis
Hello Dennis,
It seems to me that, we understand each other's position well. Sadly, right now the situation is as you explained and I can't expect a timely resolution from DX.
I will try other methods and I will keep this ticket open for a while, since I may need help during those trials.
Thank you,
Hakan
Hello Hakan,
According our policy, the ticket should be closed if you currently do not need assistance from our Support team. Please feel free to reopen it or create a new ticket if you need any further assistance. Thank you for your understanding.
Thanks,
Anatol
Hello Anatol,
According to my own policy, a support ticket should stay open until a some sort of resolution is reached whether negative or positive. Currently, I've created 9 issues, and since a resolution is reached, I've closed 6 of them myself.
I'm not abusing the open/close status of the issues and the other three (including this one) are going to stay open, as long as I'm concerned.
Hakan.
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
Hello Serge,
Thank you for the clarification.
Hakan
You are very welcome, Hakan!