Can my binding source tell me if a change has occurred?
I have a BindingSource
that I'm using in winforms data binding and I'd like to have some sort of prompt for when the user attempts to close the form after they've made changes to the data. A sort of "Are you sure you want to exit without saving changes?"
I'm aware that I can do this via the BindingSource
's CurrencyManager.ItemChanged
event by just flipping a "has changed" boolean.
However, I wan开发者_如何学编程t a more robust functionality. I'd like to know when the current data is different from the original data. The event just tells me if somethings changed. A user could still change a property, hit undo, and I would still think that there is a change in the data to save.
I want to mimic this similar functionality of notepad
- open notepad
- type something
- delete everything (essentially undoing what you did)
- close notepad, notepad closes, no prompt to save changes because it knows the end state == the initial state
If this is not possible, then should I go with the ItemChanged
event handler as outlined above or is there a better way?
For the record, I'm looking for something along the lines of
bool HasChanged()
{
return this.currentState != this.initialState;
}
not this
bool HasChanged()
{
// this._hasChanged is set to true via event handlers
return this._hasChanged;
}
I'd just rather not have to manage the current state and the initial state myself, I'm looking for a way to grab that info from the BindingSource
If I can get this functionality from the BindingSource
its way more ideal since I will be able to use the functionality on many different data sources, regardless of type, etc.
You'll have to implement the INotifyPropertyChanged
interface from within your object classes, then catch whenever a change occurs through proper event handlers for your type class within your DataSource
BindingSource
property.
The one object offering what you require is the DataSet
, containing both the Original and Current (changed) state of an persistent entity. Then, when one cancels, all you need to call is the Rollback()
method. When one accepts the changes, then a call to the AcceptChanges()
method will do.
Besides the DataSet
, perhaps considering an ORM like NHibernate will do the job for you, plus allowing you to use custom defined objects, instead of a DataSet
. Keeping the ISession
API alive while in your form will allow the ISession to keep track of your changes whatever it may be to whatever object it is, as long as it is know by NHibernate.
Another solution implementing the INotifyPropertyChanged
interface, is at the property setter, you could stock the Original value within a private field or for every property of an object. You could simple have an abstract class with the HasChanges
property return whether each property is as its Original state, then return true or false accordingly.
I have a question regarding our interesting initial discussion. I just want to make sure of one thing. Let's call it language barrier if we like. But publishing the PropertyChanged
event through the INotifyPropertyChanged
interface will also somehow "rollback" an object to its original state. The only detail you had to take care is that if the user says he doesn't want to keep the changes, then reload this CurrentItem from the underlying database via the BackgroundWorker
class and its done! No lagging from your GUI, your user has canceled the changes, and you resetted the object to its default/original state!
Well, I guess here's enough details to make yourself an idea, plus all of the other good answers provided by the others. I am confident you will find your way to accomplish what you want.
Best of success! =)
Will is right, you should implement INotifyPropertyChanged
, ideally in conjunction with IDataInfoError
to get visisble information for your users.
For your Objects to get a state and a notification on Editing, try using the IEditableObject
interface.
All three interfaces are used by default from WinForms and help make the programmers life easier.
Instead of flipping a bit, you could check the state against a snapshot of your initial state.
When you open your detail, you could make a clone of the entity that you are going to modify.
Then, when the user attempts to close the form, you could compare the clone (the entity in its original state) with the modified (or not) entity. If the clone and the entity are not equals, you could prompt the user.
You could roll your own binding source and implement it to do what you want that way you do not need INotifyChange
handling on every form - you just let the BindingSource
give you the changed element - this works when the BindingSource
is updated - bindable control .UpdateSourceTrigger
is set to UpdateOnPropertyChanged
. is instant(well almost).
Here is something to get you started - I found it on the net years ago I do not remember the originator of the code , I have modified it slightly for my purpose.
Imports System.ComponentModel.Design
Imports System.Windows.Forms
Imports System.ComponentModel
Public Class BindingSourceExIsDirty
Inherits System.Windows.Forms.BindingSource
Implements INotifyPropertyChanged
#Region "DECLARATIONS AND PROPERTIES"
Private _displayMember As String
Private _dataTable As DataTable
Private _dataSet As DataSet
Private _parentBindingSource As BindingSource
Private _form As System.Windows.Forms.Form
Private _usercontrol As System.Windows.Forms.Control
Private _isCurrentDirtyFlag As Boolean = False
Public Property IsCurrentDirty() As Boolean
Get
Return _isCurrentDirtyFlag
End Get
Set(ByVal value As Boolean)
If _isCurrentDirtyFlag <> value Then
_isCurrentDirtyFlag = value
Me.OnPropertyChanged(value.ToString())
If value = True Then 'call the event when flag is set
OnCurrentIsDirty(New EventArgs)
End If
End If
End Set
End Property
Private _objectSource As String
Public Property ObjectSource() As String
Get
Return _objectSource
End Get
Set(ByVal value As String)
_objectSource = value
Me.OnPropertyChanged(value)
End Set
End Property
' Private _autoSaveFlag As Boolean
'
' Public Property AutoSave() As Boolean
' Get
' Return _autoSaveFlag
' End Get
' Set(ByVal value As Boolean)
' _autoSaveFlag = value
' Me.OnPropertyChanged(value.ToString())
' End Set
' End Property
#End Region
#Region "EVENTS"
'Current Is Dirty Event
Public Event CurrentIsDirty As CurrentIsDirtyEventHandler
' Delegate declaration.
Public Delegate Sub CurrentIsDirtyEventHandler(ByVal sender As Object, ByVal e As EventArgs)
Protected Overridable Sub OnCurrentIsDirty(ByVal e As EventArgs)
RaiseEvent CurrentIsDirty(Me, e)
End Sub
'PropertyChanged Event
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
Protected Overridable Sub OnPropertyChanged(ByVal info As String)
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(info))
End Sub
#End Region
#Region "METHODS"
Private Sub _BindingComplete(ByVal sender As System.Object, ByVal e As System.Windows.Forms.BindingCompleteEventArgs) Handles Me.BindingComplete
If e.BindingCompleteContext = BindingCompleteContext.DataSourceUpdate Then
If e.BindingCompleteState = BindingCompleteState.Success And Not e.Binding.Control.BindingContext.IsReadOnly Then
'Make sure the data source value is refreshed (fixes problem mousing off control)
e.Binding.ReadValue()
'if not focused then not a user edit.
If Not e.Binding.Control.Focused Then Exit Sub
'check for the lookup type of combobox that changes position instead of value
If TryCast(e.Binding.Control, ComboBox) IsNot Nothing Then
'if the combo box has the same data member table as the binding source, ignore it
If CType(e.Binding.Control, ComboBox).DataSource IsNot Nothing Then
If TryCast(CType(e.Binding.Control, ComboBox).DataSource, BindingSource) IsNot Nothing Then
If CType(CType(e.Binding.Control, ComboBox).DataSource, BindingSource).DataMember = (Me.DataMember) Then
Exit Sub
End If
End If
End If
End If
IsCurrentDirty = True 'set the dirty flag because data was changed
End If
End If
End Sub
Private Sub _DataSourceChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.DataSourceChanged
_parentBindingSource = Nothing
If Me.DataSource Is Nothing Then
_dataSet = Nothing
Else
'get a reference to the dataset
Dim bsTest As BindingSource = Me
Dim dsType As Type = bsTest.DataSource.GetType
'try to cast the data source as a binding source
Do While Not TryCast(bsTest.DataSource, BindingSource) Is Nothing
'set the parent binding source reference
If _parentBindingSource Is Nothing Then _parentBindingSource = bsTest
'if cast was successful, walk up the chain until dataset is reached
bsTest = CType(bsTest.DataSource, BindingSource)
Loop
'since it is no longer a binding source, it must be a dataset or something else
If TryCast(bsTest.DataSource, DataSet) Is Nothing Then
'Cast as dataset did not work
If dsType.IsClass = False Then
Throw New ApplicationException("Invalid Binding Source ")
Else
_dataSet = Nothing
End If
Else
_dataSet = CType(bsTest.DataSource, DataSet)
End If
'is there a data member - find the datatable
If Me.DataMember <> "" Then
_DataMemberChanged(sender, e)
End If 'CType(value.GetService(GetType(IDesignerHost)), IDesignerHost)
If _form Is Nothing Then GetFormInstance()
If _usercontrol Is Nothing Then GetUserControlInstance()
End If
End Sub
Private Sub _DataMemberChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.DataMemberChanged
If Me.DataMember = "" Or _dataSet Is Nothing Then
_dataTable = Nothing
Else
'check to see if the Data Member is the name of a table in the dataset
If _dataSet.Tables(Me.DataMember) Is Nothing Then
'it must be a relationship instead of a table
Dim rel As System.Data.DataRelation = _dataSet.Relations(Me.DataMember)
If Not rel Is Nothing Then
_dataTable = rel.ChildTable
Else
Throw New ApplicationException("Invalid Data Member")
End If
Else
_dataTable = _dataSet.Tables(Me.DataMember)
End If
End If
End Sub
Public Overrides Property Site() As System.ComponentModel.ISite
Get
Return MyBase.Site
End Get
Set(ByVal value As System.ComponentModel.ISite)
'runs at design time to initiate ContainerControl
MyBase.Site = value
If value Is Nothing Then Return
' Requests an IDesignerHost service using Component.Site.GetService()
Dim service As IDesignerHost = CType(value.GetService(GetType(IDesignerHost)), IDesignerHost)
If service Is Nothing Then Return
If Not TryCast(service.RootComponent, Form) Is Nothing Then
_form = CType(service.RootComponent, Form)
ElseIf Not TryCast(service.RootComponent, UserControl) Is Nothing Then
_usercontrol = CType(service.RootComponent, UserControl)
End If
End Set
End Property
Public Function GetFormInstance() As System.Windows.Forms.Form
If _form Is Nothing And Me.CurrencyManager.Bindings.Count > 0 Then
_form = Me.CurrencyManager.Bindings(0).Control.FindForm()
End If
Return _form
End Function
''' <summary>
''' Returns the First Instance of the specified User Control
''' </summary>
''' <returns>System.Windows.Forms.Control</returns>
Public Function GetUserControlInstance() As System.Windows.Forms.Control
If _usercontrol Is Nothing And Me.CurrencyManager.Bindings.Count > 0 Then
Dim _uControls() As System.Windows.Forms.Control
_uControls = Me.CurrencyManager.Bindings(0).Control.FindForm.Controls.Find(Me.Site.Name.ToString(), True)
_usercontrol = _uControls(0)
End If
Return _usercontrol
End Function
'============================================================================
'Private Sub _PositionChanged(ByVal sender As Object, ByVal e As EventArgs) Handles Me.PositionChanged
' If IsCurrentDirty Then
' If AutoSave Then ' IsAutoSavingEvent
' Try
' 'cast table as ITableUpdate to get the Update method
' ' CType(_dataTable, ITableUpdate).Update()
' Catch ex As Exception
' ' - needs to raise an event
' End Try
' Else
' Me.CancelEdit()
' _dataTable.RejectChanges()
' End If
' IsCurrentDirty = False
' End If
'End Sub
#End Region
End Class
Yes, but there is some work involved. I know, it's a late answer, but I asked myself the same question resecently and came up with the following soltion that I wrapped up into the class UpdateManager
. I only accounted for binding to a single object so far.
This works with plain POCO objects. Implementing INotifyPropertyChanged
is not required; however, it works only if the changes are made through the UI. Changes made through code in the business object are not detected. But this enough in most cases to detect whether the object is dirty or in a saved state.
public class UpdateManager
{
public event EventHandler DirtyChanged;
private readonly BindingSource _bindingSource;
// Stores original and current values of all bindings.
private readonly Dictionary<string, (object original, object current)> _values =
new Dictionary<string, (object original, object current)>();
public UpdateManager(BindingSource bindingSource)
{
_bindingSource = bindingSource;
bindingSource.CurrencyManager.Bindings.CollectionChanged += Bindings_CollectionChanged;
bindingSource.BindingComplete += BindingSource_BindingComplete;
}
private bool _dirty;
public bool Dirty
{
get {
return _dirty;
}
set {
if (value != _dirty) {
_dirty = value;
DirtyChanged?.Invoke(this, EventArgs.Empty);
}
}
}
private void Bindings_CollectionChanged(object sender, CollectionChangeEventArgs e)
{
// Initialize the values information for the binding.
if (e.Element is Binding binding && GetCurrentValue(binding, out object value)) {
_values[binding.BindingMemberInfo.BindingField] = (value, value);
}
}
private void BindingSource_BindingComplete(object sender, BindingCompleteEventArgs e)
{
if (e.BindingCompleteContext == BindingCompleteContext.DataSourceUpdate &&
e.BindingCompleteState == BindingCompleteState.Success) {
UpdateDirty(e.Binding);
}
}
private void UpdateDirty(Binding binding)
{
if (GetCurrentValue(binding, out object currentValue)) {
string propertyName = binding.BindingMemberInfo.BindingField;
var valueInfo = _values[propertyName];
_values[propertyName] = (valueInfo.original, currentValue);
if (Object.Equals(valueInfo.original, currentValue)) {
Dirty = _values.Any(kvp => !Object.Equals(kvp.Value.original, kvp.Value.current));
} else {
Dirty = true;
}
}
}
private bool GetCurrentValue(Binding binding, out object value)
{
object model = binding.BindingManagerBase?.Current;
if (model != null) {
// Get current value in business object (model) with Reflection.
Type modelType = model.GetType();
string propertyName = binding.BindingMemberInfo.BindingField;
PropertyInfo modelProp = modelType.GetProperty(propertyName);
value = modelProp.GetValue(model);
return true;
}
value = null;
return false;
}
}
In the form I used it like this:
private UpdateManager _updateManager;
private Person _person = new Person();
public frmBindingNotification()
{
InitializeComponent();
_updateManager = new UpdateManager(personBindingSource);
_updateManager.DirtyChanged += UpdateManager_DirtyChanged;
personBindingSource.DataSource = _person; // Assign the current business object.
}
private void UpdateManager_DirtyChanged(object sender, EventArgs e)
{
Console.WriteLine(_updateManager.Dirty ? "Dirty" : "Saved"); // Testing only.
}
Whenever the Dirty
status changes, this prints either "Dirty" or "Saved" in the Output window.
精彩评论