c# - son - ¿Puede mi fuente de enlace decirme si ha ocurrido un cambio?
la evaporacion condensacion y ebullicion son exclusivamente cambios quimicos (6)
Tengo un BindingSource
que estoy usando en el enlace de datos de winforms y me gustaría tener algún tipo de aviso para cuando el usuario intente cerrar el formulario después de haber realizado cambios en los datos. Una especie de "¿Está seguro de que desea salir sin guardar los cambios?"
Soy consciente de que puedo hacer esto a través del evento CurrencyManager.ItemChanged
BindingSource
simplemente cambiando un booleano "ha cambiado".
Sin embargo, quiero una funcionalidad más robusta. Me gustaría saber cuándo los datos actuales son diferentes de los datos originales. El evento solo me dice si algo ha cambiado. Un usuario todavía podría cambiar una propiedad, hacer clic en deshacer, y todavía pensaría que hay un cambio en los datos para guardar.
Quiero imitar esta funcionalidad similar de bloc de notas
- abrir bloc de notas
- escribe algo
- borra todo (esencialmente deshaciendo lo que hiciste)
- cierre el bloc de notas, el bloc de notas se cierra, no hay mensajes para guardar los cambios porque conoce el estado final == el estado inicial
Si esto no es posible, ¿debería ItemChanged
controlador de eventos ItemChanged
como se describe anteriormente o hay una mejor manera?
Para el registro, estoy buscando algo en la línea de
bool HasChanged()
{
return this.currentState != this.initialState;
}
no esta
bool HasChanged()
{
// this._hasChanged is set to true via event handlers
return this._hasChanged;
}
Preferiría no tener que administrar el estado actual y el estado inicial, estoy buscando una forma de obtener esa información de BindingSource
Si puedo obtener esta funcionalidad de BindingSource
su forma más ideal, ya que podré para utilizar la funcionalidad en muchos orígenes de datos diferentes, independientemente del tipo, etc.
Cuando abre sus detalles, puede hacer un clon de la entidad que va a modificar.
Luego, cuando el usuario intenta cerrar el formulario, puede comparar el clon (la entidad en su estado original) con la entidad modificada (o no). Si el clon y la entidad no son iguales, puede preguntar al usuario.
En lugar de voltear un poco, puede comparar el estado con una instantánea de su estado inicial.
Podría rodar su propio origen de enlace e implementarlo para hacer lo que quiera de esa manera, ya que no necesita el manejo de INotifyChange
en todos los formularios; simplemente deje que BindingSource
brinde el elemento modificado; esto funciona cuando se actualiza .UpdateSourceTrigger
control .UpdateSourceTrigger
se establece en UpdateOnPropertyChanged
. Es instantáneo (bueno, casi).
Aquí hay algo para que comience: lo encontré en la red hace años. No recuerdo el creador del código, lo he modificado ligeramente para mi propósito.
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
Tendrá que implementar la interfaz INotifyPropertyChanged
desde sus clases de objetos y luego detectar cada vez que se produzca un cambio a través de los controladores de eventos adecuados para su clase de tipo dentro de su propiedad DataSource
BindingSource
.
El único objeto que ofrece lo que necesita es el DataSet
, que contiene el estado original y actual (modificado) de una entidad persistente. Luego, cuando uno cancela, todo lo que necesita llamar es el método Rollback()
. Cuando uno acepta los cambios, se realizará una llamada al método AcceptChanges()
.
Además del DataSet
, tal vez considerando que un ORM como NHibernate hará el trabajo por usted, además de permitirle usar objetos personalizados definidos, en lugar de un DataSet
. Mantener viva la API de ISession
mientras está en su formulario le permitirá a ISession realizar un seguimiento de sus cambios, sea cual sea el objeto, siempre que sea conocido por NHibernate.
Otra solución que implementa la interfaz INotifyPropertyChanged
está en el establecedor de propiedades, puede almacenar el valor original dentro de un campo privado o para cada propiedad de un objeto. HasChanges
podría tener una clase abstracta con la propiedad HasChanges
devolver si cada propiedad es como su estado Original, y luego devolver verdadero o falso en consecuencia.
Tengo una pregunta sobre nuestra interesante discusión inicial. Solo quiero asegurarme de una cosa. Llamémoslo barrera del lenguaje si nos gusta. Pero la publicación del evento PropertyChanged
través de la interfaz INotifyPropertyChanged
también "revertirá" un objeto a su estado original. El único detalle que tuvo que tener en cuenta es que si el usuario dice que no desea mantener los cambios, vuelva a cargar este CurrentItem desde la base de datos subyacente a través de la clase BackgroundWorker
y listo! ¡No se ha retrasado su GUI, su usuario ha cancelado los cambios y ha restablecido el objeto a su estado predeterminado / original!
Bueno, supongo que aquí hay suficientes detalles para hacerse una idea, además de todas las otras buenas respuestas proporcionadas por los demás. Estoy seguro de que encontrará la manera de lograr lo que quiere.
¡Lo mejor del éxito! =)
Will tiene razón, debe implementar INotifyPropertyChanged
, idealmente junto con IDataInfoError
para obtener información visible para sus usuarios.
Para que sus objetos obtengan un estado y una notificación sobre la edición, intente utilizar la interfaz IEditableObject
.
Las tres interfaces se utilizan de forma predeterminada desde WinForms y ayudan a facilitar la vida de los programadores.
Sí , pero hay algún trabajo involucrado. Lo sé, es una respuesta tardía, pero me hice la misma pregunta resecentemente y se me ocurrió la siguiente solución que envolví en la clase UpdateManager
. Solo he tenido en cuenta el enlace a un solo objeto hasta ahora.
Esto funciona con objetos planos de POCO. Implementar INotifyPropertyChanged
no es obligatorio; sin embargo, funciona solo si los cambios se realizan a través de la interfaz de usuario. Los cambios realizados a través del código en el objeto de negocio no se detectan. Pero esto es suficiente en la mayoría de los casos para detectar si el objeto está sucio o en un estado guardado.
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;
}
}
En la forma lo usé así:
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.
}
Siempre que cambie el estado Dirty
, esto imprime "Sucio" o "Guardado" en la ventana de Salida.