5

This is more a conceptual question. Here is my current predicament; I am writing a vb.net WPF application and using the MVVM pattern (love it! maintainability is just amazingly awesome). Currently all the code is written by hand and there is no use of NHibernate or Entity Framework as the backend is an access database (due to policy I cannot use NH and EF doesn't support JET Databases, we may switch to MSSQL at some point but that may be a while from now).

The application is running quite well and was wondering what is the best way to send updates back to the database.

Currently the method is to add a boolean to a record on the set portion of the model to "dirty" then when update is pressed we loop through all the records that are "dirty" and use oledbcommand (execute with parameters) sql statements to update.

This create an excellent separation of concerns but if this is the wrong way I would like to know alternatives (please note the database type and the associated drawbacks such is it not working with EF).

Thanks!

Final code in VB.NET after comments etc:

Public Class Car
Implements ICloneable

Public Property Make() As String
    Get
        Return m_Make
    End Get
    Set(ByVal value As String)
        m_Make = value
    End Set
End Property
Private m_Make As String

Public Property Model() As String
    Get
        Return m_Model
    End Get
    Set(ByVal value As String)
        m_Model = value
    End Set
End Property
Private m_Model As String

Public Function Clone() As Object Implements System.ICloneable.Clone
    Return New Car() With { _
     .Make = Me.Make, _
     .Model = Me.Model _
    }
End Function
End Class



Public Class CarEqualityComparer
Implements IEqualityComparer(Of Car)

Public Overloads Function Equals(ByVal x As Car, ByVal y As Car) As Boolean Implements System.Collections.Generic.IEqualityComparer(Of Car).Equals
    Return x.Make = y.Make AndAlso x.Model = y.Model
End Function

Public Overloads Function GetHashCode(ByVal obj As Car) As Integer Implements System.Collections.Generic.IEqualityComparer(Of Car).GetHashCode
    Return 1 'http://blogs.msdn.com/b/jaredpar/archive/2008/06/03/making-equality-easier.aspx
End Function

End Class

Public Class CarRepository
    Private _carComparator As New CarEqualityComparer

    Private _cars As New ChangeTracker(Of Car)(_carComparator)

    Public Function GetCars() As IEnumerable(Of Car)
        'TODO: JET/ADO code here, you would obviously do in a for/while loop
        Dim dbId1 As Integer = 1
        Dim make1 As String = "Ford"
        Dim model1 As String = "Focus"

        Dim dbId2 As Integer = 2
        Dim make2 As String = "Hyundai"
        Dim model2 As String = "Elantra"

        'TODO: create or update car objects
        Dim car1 As Car
        If Not _cars.IsTracking(dbId1) Then
            car1 = New Car()
        Else
            car1 = _cars.GetItem(dbId1)
        End If

        car1.Make = make1
        car1.Model = model1

        If Not _cars.IsTracking(dbId1) Then
            _cars.StartTracking(dbId1, car1)
        End If


        Dim car2 As Car
        If Not _cars.IsTracking(dbId2) Then
            car2 = New Car()
        Else
            car2 = _cars.GetItem(dbId2)
        End If

        car2.Make = make2
        car2.Model = model2

        If Not _cars.IsTracking(dbId2) Then
            _cars.StartTracking(dbId2, car2)
        End If

        Return _cars.GetTrackedItems()
    End Function

    Public Sub SaveCars(ByVal cars As IEnumerable(Of Car))

        'TODO: JET/ADO code here to update the item
        Console.WriteLine("Distinct " & cars.Distinct.Count.ToString)

        For Each changedItem As Car In _cars.GetChangedItems().Intersect(cars)
            Console.Write("Saving: ")
            Console.WriteLine(changedItem.Make)
        Next

        For Each newItem As Car In cars.Except(_cars.GetTrackedItems())
            Console.Write("Adding: ")
            Console.WriteLine(newItem.Make)
            Dim newId As Integer = CInt(Math.Ceiling(Rnd() * 5000)) 'Random right now but JET/ADO to get the id later....
            _cars.StartTracking(newId, newItem)
        Next

        Dim removalArray As New ArrayList
        For Each deletedItem As Car In _cars.GetTrackedItems().Except(cars)
            Console.Write("Removing: ")
            Console.WriteLine(deletedItem.Make)
            removalArray.Add(_cars.GetId(deletedItem)) 'Cannot remove right as iterating through array - clearly that would be problematic....
        Next
        For Each dbId As Integer In removalArray
            _cars.StopTracking(dbId)
        Next

        _cars.SetNewCheckpoint()

    End Sub
End Class

Public Class ChangeTracker(Of T As {ICloneable})
    'item "checkpoints" that are internal to this list
    Private _originals As New Dictionary(Of Integer, T)()
    Private _originalIndex As New Dictionary(Of T, Integer)()

    'the current, live-edited objects
    Private _copies As New Dictionary(Of Integer, T)()
    Private _copyIndex As New Dictionary(Of T, Integer)()

    Private _comparator As System.Collections.Generic.IEqualityComparer(Of T)

    Public Sub New(ByVal comparator As System.Collections.Generic.IEqualityComparer(Of T))
        _comparator = comparator
    End Sub

    Public Function IsChanged(ByVal copy As T) As Boolean
        Dim original = _originals(_copyIndex(copy))

        Return Not _comparator.Equals(copy, original)

    End Function

    Public Function GetChangedItems() As IEnumerable(Of T)
        Dim items As IEnumerable(Of T)
        items = _copies.Values.Where(Function(c) IsChanged(c))
        Return items
    End Function

    Public Function GetTrackedItems() As IEnumerable(Of T)
        Return _copies.Values
    End Function

    Public Sub SetNewCheckpoint()
        For Each copy In Me.GetChangedItems().ToList()
            Dim dbId As Integer = _copyIndex(copy)
            Dim oldOriginal = _originals(dbId)
            Dim newOriginal = DirectCast(copy.Clone(), T)

            _originals(dbId) = newOriginal
            _originalIndex.Remove(oldOriginal)
            _originalIndex.Add(newOriginal, dbId)
        Next
    End Sub

    Public Sub StartTracking(ByVal dbId As Integer, ByVal item As T)
        Dim newOriginal = DirectCast(item.Clone(), T)
        _originals(dbId) = newOriginal
        _originalIndex(newOriginal) = dbId

        _copies(dbId) = item
        _copyIndex(item) = dbId
    End Sub

    Public Sub StopTracking(ByVal dbId As Integer)
        Dim original = _originals(dbId)
        Dim copy = _copies(dbId)

        _copies.Remove(dbId)
        _originals.Remove(dbId)
        _copyIndex.Remove(copy)
        _originalIndex.Remove(original)
    End Sub

    Public Function IsTracking(ByVal dbId As Integer) As Boolean
        Return _originals.ContainsKey(dbId)
    End Function

    Public Function IsTracking(ByVal item As T) As Boolean
        Return _copyIndex.ContainsKey(item)
    End Function

    Public Function GetItem(ByVal dbId As Integer) As T
        Return _copies(dbId)
    End Function

    Public Function GetId(ByVal item As T) As Integer
        Dim dbId As Integer = (_copyIndex(item))
        Return dbId
    End Function

End Class
Omar Mir
  • 1,500
  • 1
  • 19
  • 39

1 Answers1

3

Since you are using an Update/Save button to commit you changes to the database, I would recommend using a Repository-like pattern, where the Repository tracks changes whenever it performs save operations.

This is similar to how Entity Framework implements self-tracking entities (STE). In EF STE, a tracker object is created for each entity you want to track that listens to events similar to PropertyChanged in order to determine if an object is 'dirty'.

The major benefit to this approach is that you can perform batch update/deletes without needing to store any persistence states with your Models or ViewModels, or having to always save everything you have to the DB. This provides even greater separation of concerns (DAL vs M vs VM vs V). I find that MVVM and the Repository Pattern go very well together.

Here's the overall approach:

  1. You load items from the database from within a Repository. As you load items, you store them in a "tracker" object that retains a copy of the object as it was originally stored in the database, as well as a relationship to the "live" (editable) object. We call this process "creating a checkpoint".
  2. You use the editable objects in your MVVM as usual, allowing the user to make any changes they want. You don't need to track any changes.
  3. When the user clicks the 'Save' button, you send all of the objects on screen back into the repository to be saved.
  4. The Repository checks each object against the original copies and determines which items are "dirty".
  5. Only the dirty items are saved to the database.
  6. Once the save is successful, you create a new checkpoint.

Here's some sample code I whipped up:

First, here's a sample class called Car that we will use in our Repository. Notice that there is no Dirty property on the object.

public class Car : IEquatable<Car>, ICloneable
{
    public string Make { get; set; }
    public string Model { get; set; }

    public bool Equals(Car other)
    {
        return other.Make == this.Make &&
               other.Model == this.Model;
    }

    public object Clone()
    {
        return new Car { Make = this.Make, Model = this.Model };
    }
}

Next, here is a CarRepository that you will use to initialize objects from your database:

public class CarRepository
{
    private ChangeTracker<Car> _cars = new ChangeTracker<Car>();

    public IEnumerable<Car> GetCars()
    {
        //TODO: JET/ADO code here, you would obviously do in a for/while loop
        int dbId1 = 1;
        string make1 = "Ford";
        string model1 = "Focus";

        //TODO: create or update car objects
        Car car1;
        if (!_cars.IsTracking(dbId1))
            car1 = new Car();
        else
            car1 = _cars.GetItem(dbId1);

        car1.Make = make1;
        car1.Model = model1;

        if (!_cars.IsTracking(dbId1))
            _cars.StartTracking(dbId1, car1);

        return _cars.GetTrackedItems();
    }

    public void SaveCars(IEnumerable<Car> cars)
    {
        foreach (var changedItem in _cars.GetChangedItems().Intersect(cars))
        {
            //TODO: JET/ADO code here to update the item
        }

        foreach (var newItem in cars.Except(_cars.GetTrackedItems()))
        {
            //TODO: JET/ADO code here to add the item to the DB and get its new ID
            int newId = 5;
            _cars.StartTracking(newId, newItem);
        }            

        _cars.SetNewCheckpoint();
    }
}

Lastly, there is a helper class that the Repository uses to track changes and set checkpoints called ChangeTracker.

public class ChangeTracker<T> where T : IEquatable<T>, ICloneable
{
    //item "checkpoints" that are internal to this list
    private Dictionary<int, T> _originals = new Dictionary<int, T>();
    private Dictionary<T, int> _originalIndex = new Dictionary<T, int>();

    //the current, live-edited objects
    private Dictionary<int, T> _copies = new Dictionary<int, T>();
    private Dictionary<T, int> _copyIndex = new Dictionary<T, int>();

    public bool IsChanged(T copy)
    {
        var original = _originals[_copyIndex[copy]];
        return original.Equals(copy);
    }

    public IEnumerable<T> GetChangedItems()
    {
        return _copies.Values.Where(c => IsChanged(c));
    }

    public IEnumerable<T> GetTrackedItems()
    {
        return _copies.Values;
    }

    public void SetNewCheckpoint()
    {
        foreach (var copy in this.GetChangedItems().ToList())
        {
            int dbId = _copyIndex[copy];
            var oldOriginal = _originals[dbId];
            var newOriginal = (T)copy.Clone();

            _originals[dbId] = newOriginal;
            _originalIndex.Remove(oldOriginal);
            _originalIndex.Add(newOriginal, dbId);
        }
    }

    public void StartTracking(int dbId, T item)
    {
        var newOriginal = (T)item.Clone();
        _originals[dbId] = newOriginal;
        _originalIndex[newOriginal] = dbId;

        _copies[dbId] = item;
        _copyIndex[item] = dbId;
    }

    public void StopTracking(int dbId)
    {
        var original = _originals[dbId];
        var copy = _copies[dbId];

        _copies.Remove(dbId);
        _originals.Remove(dbId);
        _copyIndex.Remove(copy);
        _originalIndex.Remove(original);
    }

    public bool IsTracking(int dbId)
    {
        return _originals.ContainsKey(dbId);
    }

    public bool IsTracking(T item)
    {
        return _copyIndex.ContainsKey(item);
    }

    public T GetItem(int dbId)
    {
        return _liveCopies[dbId];
    }
}

And, here's how you would use your Repository in a program:

static void Main(string[] args)
{
    var repository = new CarRepository();

    var cars = repository.GetCars().ToArray();

    //make some arbitrary changes...
    cars[0].Make = "Chevy";
    cars[1].Model = "Van";

    //when we call SaveCars, the repository will detect that
    //both of these cars have changed, and write them to the database
    repository.SaveCars(cars);
}

This naive implementation relies on IEquatable and ICloneable, though these are certainly not necessary and there are likely better ways of doing things or you may have a more efficient way of determining whether an item has changed. (For example, the idea of creating object copies is not exactly memory-friendly). You'll also need to deal with deleted items as well, but that would be easy to add to the sample above.

Kevin McCormick
  • 2,358
  • 20
  • 20
  • Hi Kevin, I'm only half understanding this - could you explain what is actually happening in that code? Kind of like a narrative? – Omar Mir Mar 14 '12 at 19:50
  • Hi Omar, I made a few edits, I hope that is helpful. The very short version is that we are still doing Dirty tracking, except we just calculate which objects are dirty when the user clicks Save, instead of managing dirty flags as the user makes changes. – Kevin McCormick Mar 14 '12 at 20:09
  • Hey Kevin, What about ovveriding GetHashCode? – Omar Mir Mar 19 '12 at 01:19
  • Based on some research I've done, actually the better approach would be to use IEqualityComparer. See this question: http://stackoverflow.com/questions/9265269/implementing-iequatablet-in-a-mutable-type – Kevin McCormick Mar 19 '12 at 15:50
  • Hi Kevin, Done! Could you have a look at this and let me know if it fits within what you were expecting? https://gist.github.com/2138528 – Omar Mir Mar 20 '12 at 17:42
  • Updated gist with delete item code - if you think that works I will post it my answer for the vb peoples :) – Omar Mir Mar 20 '12 at 21:28
  • I ended up implementing a generic IEqualityComparer and for clone I just used memberwiseclone. Saved a lot of code :) - works great Kevin, thanks. – Omar Mir Mar 23 '12 at 04:27
  • Memberwiseclone wasn't a good idea - wrote my own clone functions. – Omar Mir Apr 04 '12 at 01:17
  • Haha yeah, I've had that experience too. MemberwiseClone works in only a very small percentage of cases. – Kevin McCormick Apr 04 '12 at 14:09