正确的方法来更新MVVM模式中的记录以获得最大效率

Oma*_*Mir 5 c# vb.net mvvm visual-studio

这是一个概念性问题.这是我目前的困境; 我正在编写一个vb.net WPF应用程序并使用MVVM模式(喜欢它!可维护性非常棒).目前所有代码都是手工编写的,没有使用NHibernate或Entity Framework,因为后端是一个访问数据库(由于策略我不能使用NH和EF不支持JET数据库,我们可能会在某些时候切换到MSSQL但从现在开始可能还需要一段时间).

应用程序运行良好,并想知道将更新发送回数据库的最佳方法是什么.

目前的方法是将模型的set部分上的记录添加一个布尔值为"脏"然后当按下更新时,我们遍历所有"脏"的记录并使用oledbcommand(用参数执行)sql语句来更新.

这创造了一个很好的关注点分离,但如果这是错误的方式我想知道替代方案(请注意数据库类型和相关的缺点,如它不适用于EF).

谢谢!

评论后的VB.NET中的最终代码:

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
Run Code Online (Sandbox Code Playgroud)

Kev*_*ick 3

由于您使用“更新/保存”按钮将更改提交到数据库,因此我建议使用类似存储库的模式,其中存储库每当执行保存操作时都会跟踪更改。

这类似于实体框架实现自我跟踪实体(STE)的方式。在 EF STE 中,为您想要跟踪的每个实体创建一个跟踪器对象,该对象侦听类似的事件,PropertyChanged以确定对象是否“脏”。

这种方法的主要好处是,您可以执行批量更新/删除,而无需使用模型或视图模型存储任何持久性状态,或者必须始终将所有内容保存到数据库中。这提供了更大的关注点分离(DAL、M、VM、V)。我发现 MVVM 和存储库模式可以很好地结合在一起。

总体方法如下:

  1. 您可以从存储库中的数据库加载项目。当您加载项目时,您将它们存储在“跟踪器”对象中,该对象保留最初存储在数据库中的对象的副本,以及与“实时”(可编辑)对象的关系。我们将此过程称为“创建检查点”。
  2. 您像平常一样使用 MVVM 中的可编辑对象,允许用户进行他们想要的任何更改。您不需要跟踪任何更改。
  3. 当用户单击“保存”按钮时,您将屏幕上的所有对象发送回存储库以进行保存。
  4. 存储库根据原始副本检查每个对象并确定哪些项目是“脏”的。
  5. 只有脏项目才会保存到数据库中。
  6. 保存成功后,您将创建一个新的检查点。

这是我编写的一些示例代码:

首先,这是一个名为的示例类Car,我们将在存储库中使用它。请注意,该对象上没有 Dirty 属性。

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 };
    }
}
Run Code Online (Sandbox Code Playgroud)

接下来,您将使用以下命令CarRepository来初始化数据库中的对象:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,存储库使用一个辅助类来跟踪更改并设置名为 的检查点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];
    }
}
Run Code Online (Sandbox Code Playgroud)

并且,以下是您在程序中使用存储库的方法:

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);
}
Run Code Online (Sandbox Code Playgroud)

这种幼稚的实现依赖于 IEquatable 和 ICloneable,尽管这些当然不是必需的,并且可能有更好的方法来执行操作,或者您可能有更有效的方法来确定项目是否已更改。(例如,创建对象副本的想法并不完全是内存友好的)。您还需要处理已删除的项目,但这很容易添加到上面的示例中。