using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; using System.Text; namespace System.Data.Linq { using System.Data.Linq.Mapping; using System.Data.Linq.Provider; internal abstract class ChangeTracker { /// /// Starts tracking an object as 'unchanged' /// /// /// internal abstract TrackedObject Track(object obj); /// /// Starts tracking an object as 'unchanged', and optionally /// 'weakly' tracks all other referenced objects recursively. /// /// /// True if all untracked objects in the graph /// should be tracked recursively. /// internal abstract TrackedObject Track(object obj, bool recurse); /// /// Fast-tracks an object that is already in identity cache /// /// internal abstract void FastTrack(object obj); internal abstract bool IsTracked(object obj); internal abstract TrackedObject GetTrackedObject(object obj); internal abstract void StopTracking(object obj); internal abstract void AcceptChanges(); internal abstract IEnumerable GetInterestingObjects(); internal static ChangeTracker CreateChangeTracker(CommonDataServices dataServices, bool asReadOnly) { if (asReadOnly) { return new ReadOnlyChangeTracker(); } else { return new StandardChangeTracker(dataServices); } } class StandardChangeTracker : ChangeTracker { Dictionary items; PropertyChangingEventHandler onPropertyChanging; CommonDataServices services; internal StandardChangeTracker(CommonDataServices services) { this.services = services; this.items = new Dictionary(); this.onPropertyChanging = new PropertyChangingEventHandler(this.OnPropertyChanging); } /// /// Given a type root and a discriminator, return the type that would be instantiated. /// private static MetaType TypeFromDiscriminator(MetaType root, object discriminator) { foreach (MetaType type in root.InheritanceTypes) { if (IsSameDiscriminator(discriminator, type.InheritanceCode)) return type; } return root.InheritanceDefault; } private static bool IsSameDiscriminator(object discriminator1, object discriminator2) { if (discriminator1 == discriminator2) { return true; } if (discriminator1 == null || discriminator2 == null) { return false; } return discriminator1.Equals(discriminator2); } internal override TrackedObject Track(object obj) { return Track(obj, false); } internal override TrackedObject Track(object obj, bool recurse) { MetaType type = this.services.Model.GetMetaType(obj.GetType()); Dictionary visited = new Dictionary(); return Track(type, obj, visited, recurse, 1); } private TrackedObject Track(MetaType mt, object obj, Dictionary visited, bool recurse, int level) { StandardTrackedObject tracked = (StandardTrackedObject)this.GetTrackedObject(obj); if (tracked != null || visited.ContainsKey(obj)) { return tracked; } // The root object tracked is tracked normally - all other objects // in the reference graph are weakly tracked. bool weaklyTrack = level > 1; tracked = new StandardTrackedObject(this, mt, obj, obj, weaklyTrack); if (tracked.HasDeferredLoaders) { throw Error.CannotAttachAddNonNewEntities(); } this.items.Add(obj, tracked); this.Attach(obj); visited.Add(obj, obj); if (recurse) { // track parents (objects we are dependent on) foreach (RelatedItem parent in this.services.GetParents(mt, obj)) { this.Track(parent.Type, parent.Item, visited, recurse, level + 1); } // track children (objects that are dependent on us) foreach (RelatedItem child in this.services.GetChildren(mt, obj)) { this.Track(child.Type, child.Item, visited, recurse, level + 1); } } return tracked; } internal override void FastTrack(object obj) { // assumes object is already in identity cache this.Attach(obj); } internal override void StopTracking(object obj) { this.Detach(obj); this.items.Remove(obj); } internal override bool IsTracked(object obj) { return this.items.ContainsKey(obj) || this.IsFastTracked(obj); } private bool IsFastTracked(object obj) { MetaType type = this.services.Model.GetTable(obj.GetType()).RowType; return this.services.IsCachedObject(type, obj); } internal override TrackedObject GetTrackedObject(object obj) { StandardTrackedObject ti; if (!this.items.TryGetValue(obj, out ti)) { if (this.IsFastTracked(obj)) { return this.PromoteFastTrackedObject(obj); } } return ti; } private StandardTrackedObject PromoteFastTrackedObject(object obj) { Type type = obj.GetType(); MetaType metaType = this.services.Model.GetTable(type).RowType.GetInheritanceType(type); return this.PromoteFastTrackedObject(metaType, obj); } private StandardTrackedObject PromoteFastTrackedObject(MetaType type, object obj) { StandardTrackedObject ti = new StandardTrackedObject(this, type, obj, obj); this.items.Add(obj, ti); return ti; } private void Attach(object obj) { INotifyPropertyChanging notifier = obj as INotifyPropertyChanging; if (notifier != null) { notifier.PropertyChanging += this.onPropertyChanging; } else { // if has no notifier, consider it modified already this.OnPropertyChanging(obj, null); } } private void Detach(object obj) { INotifyPropertyChanging notifier = obj as INotifyPropertyChanging; if (notifier != null) { notifier.PropertyChanging -= this.onPropertyChanging; } } private void OnPropertyChanging(object sender, PropertyChangingEventArgs args) { StandardTrackedObject ti; if (this.items.TryGetValue(sender, out ti)) { ti.StartTracking(); } else if (this.IsFastTracked(sender)) { ti = this.PromoteFastTrackedObject(sender); ti.StartTracking(); } } internal override void AcceptChanges() { List list = new List((IEnumerable)this.items.Values); foreach (TrackedObject item in list) { item.AcceptChanges(); } } internal override IEnumerable GetInterestingObjects() { foreach (StandardTrackedObject ti in this.items.Values) { if (ti.IsInteresting) { yield return ti; } } } class StandardTrackedObject : TrackedObject { private StandardChangeTracker tracker; private MetaType type; private object current; private object original; private State state; private BitArray dirtyMemberCache; private bool haveInitializedDeferredLoaders; private bool isWeaklyTracked; enum State { New, Deleted, PossiblyModified, Modified, Removed, Dead } public override string ToString() { return type.Name + ":" + GetState(); } private string GetState() { switch (this.state) { case State.New: case State.Deleted: case State.Dead: case State.Removed: return this.state.ToString(); default: if (this.IsModified) { return "Modified"; } else { return "Unmodified"; } } } internal StandardTrackedObject(StandardChangeTracker tracker, MetaType type, object current, object original) { if (current == null) { throw Error.ArgumentNull("current"); } this.tracker = tracker; this.type = type.GetInheritanceType(current.GetType()); this.current = current; this.original = original; this.state = State.PossiblyModified; dirtyMemberCache = new BitArray(this.type.DataMembers.Count); } internal StandardTrackedObject(StandardChangeTracker tracker, MetaType type, object current, object original, bool isWeaklyTracked) : this(tracker, type, current, original) { this.isWeaklyTracked = isWeaklyTracked; } internal override bool IsWeaklyTracked { get { return isWeaklyTracked; } } internal override MetaType Type { get { return this.type; } } internal override object Current { get { return this.current; } } internal override object Original { get { return this.original; } } internal override bool IsNew { get { return this.state == State.New; } } internal override bool IsDeleted { get { return this.state == State.Deleted; } } internal override bool IsRemoved { get { return this.state == State.Removed; } } internal override bool IsDead { get { return this.state == State.Dead; } } internal override bool IsModified { get { return this.state == State.Modified || (this.state == State.PossiblyModified && this.current != this.original && this.HasChangedValues()); } } internal override bool IsUnmodified { get { return this.state == State.PossiblyModified && (this.current == this.original || !this.HasChangedValues()); } } internal override bool IsPossiblyModified { get { return this.state == State.Modified || this.state == State.PossiblyModified; } } internal override bool CanInferDelete() { // A delete can be inferred iff there is a non-nullable singleton association that has // been set to null, and the association has DeleteOnNull = true. if (this.state == State.Modified || this.state == State.PossiblyModified) { foreach (MetaAssociation assoc in Type.Associations) { if (assoc.DeleteOnNull && assoc.IsForeignKey && !assoc.IsNullable && !assoc.IsMany && assoc.ThisMember.StorageAccessor.HasAssignedValue(Current) && assoc.ThisMember.StorageAccessor.GetBoxedValue(Current) == null) { return true; } } } return false; } internal override bool IsInteresting { get { return this.state == State.New || this.state == State.Deleted || this.state == State.Modified || (this.state == State.PossiblyModified && this.current != this.original) || CanInferDelete(); } } internal override void ConvertToNew() { // must be new or unmodified or removed to convert to new System.Diagnostics.Debug.Assert(this.IsNew || this.IsRemoved || this.IsUnmodified); this.original = null; this.state = State.New; } internal override void ConvertToPossiblyModified() { System.Diagnostics.Debug.Assert(this.IsPossiblyModified || this.IsDeleted); this.state = State.PossiblyModified; this.isWeaklyTracked = false; } internal override void ConvertToModified() { System.Diagnostics.Debug.Assert(this.IsPossiblyModified); System.Diagnostics.Debug.Assert(this.type.VersionMember != null || !this.type.HasUpdateCheck); this.state = State.Modified; this.isWeaklyTracked = false; } internal override void ConvertToPossiblyModified(object originalState) { // must be modified or unmodified to convert to modified System.Diagnostics.Debug.Assert(this.IsNew || this.IsPossiblyModified); System.Diagnostics.Debug.Assert(originalState != null); System.Diagnostics.Debug.Assert(originalState.GetType() == this.type.Type); this.state = State.PossiblyModified; this.original = this.CreateDataCopy(originalState); this.isWeaklyTracked = false; } internal override void ConvertToDeleted() { // must be modified or unmodified to be deleted System.Diagnostics.Debug.Assert(this.IsDeleted || this.IsPossiblyModified); this.state = State.Deleted; this.isWeaklyTracked = false; } internal override void ConvertToDead() { System.Diagnostics.Debug.Assert(this.IsDead || this.IsDeleted); this.state = State.Dead; this.isWeaklyTracked = false; } internal override void ConvertToRemoved() { System.Diagnostics.Debug.Assert(this.IsRemoved || this.IsNew); this.state = State.Removed; this.isWeaklyTracked = false; } internal override void ConvertToUnmodified() { System.Diagnostics.Debug.Assert(this.IsNew || this.IsPossiblyModified); // reset to unmodified this.state = State.PossiblyModified; if (this.current is INotifyPropertyChanging) { this.original = this.current; } else { this.original = this.CreateDataCopy(this.current); } this.ResetDirtyMemberTracking(); this.isWeaklyTracked = false; } internal override void AcceptChanges() { if (IsWeaklyTracked) { InitializeDeferredLoaders(); isWeaklyTracked = false; } if (this.IsDeleted) { this.ConvertToDead(); } else if (this.IsNew) { this.InitializeDeferredLoaders(); this.ConvertToUnmodified(); } else if (this.IsPossiblyModified) { this.ConvertToUnmodified(); } } private void AssignMember(object instance, MetaDataMember mm, object value) { // In the unnotified case, directly use the storage accessor // for everything because there are not events to be fired. if (!(this.current is INotifyPropertyChanging)) { mm.StorageAccessor.SetBoxedValue(ref instance, value); } else { // Go through the member accessor to fire events. mm.MemberAccessor.SetBoxedValue(ref instance, value); } } /// /// Certain state is saved during change tracking to enable modifications /// to be detected taking refresh operations into account. When changes /// are reverted or accepted, this state must be reset. /// private void ResetDirtyMemberTracking() { this.dirtyMemberCache.SetAll(false); } /// /// Refresh internal tracking state using the original value and mode /// specified. /// internal override void Refresh(RefreshMode mode, object freshInstance) { this.SynchDependentData(); // This must be done prior to updating original values this.UpdateDirtyMemberCache(); // Apply the refresh strategy to each data member Type instanceType = freshInstance.GetType(); foreach (MetaDataMember mm in type.PersistentDataMembers) { var memberMode = mm.IsDbGenerated ? RefreshMode.OverwriteCurrentValues : mode; if (memberMode != RefreshMode.KeepCurrentValues) { if (!mm.IsAssociation && (this.Type.Type == instanceType || mm.DeclaringType.Type.IsAssignableFrom(instanceType))) { object freshValue = mm.StorageAccessor.GetBoxedValue(freshInstance); this.RefreshMember(mm, memberMode, freshValue); } } } // Make the new data the current original value this.original = this.CreateDataCopy(freshInstance); if (mode == RefreshMode.OverwriteCurrentValues) { this.ResetDirtyMemberTracking(); } } /// /// Using the last saved comparison baseline, figure out which members have /// changed since the last refresh, and save that information. This must be /// done BEFORE any merge operations modify the current values. /// private void UpdateDirtyMemberCache() { // iterate over all members, and if they differ from // last read values, mark as dirty foreach (MetaDataMember mm in type.PersistentDataMembers) { if (mm.IsAssociation && mm.Association.IsMany) { continue; } if (!this.dirtyMemberCache.Get(mm.Ordinal) && this.HasChangedValue(mm)) { this.dirtyMemberCache.Set(mm.Ordinal, true); } } } internal override void RefreshMember(MetaDataMember mm, RefreshMode mode, object freshValue) { System.Diagnostics.Debug.Assert(!mm.IsAssociation); if (mode == RefreshMode.KeepCurrentValues) { return; } bool hasUserChange = this.HasChangedValue(mm); // we don't want to overwrite any modified values, unless // the mode is original wins if (hasUserChange && mode != RefreshMode.OverwriteCurrentValues) return; object currentValue = mm.StorageAccessor.GetBoxedValue(this.current); if (!object.Equals(freshValue, currentValue)) { mm.StorageAccessor.SetBoxedValue(ref this.current, freshValue); // update all singleton associations that are affected by a change to this member foreach (MetaDataMember am in this.GetAssociationsForKey(mm)) { if (!am.Association.IsMany) { IEnumerable ds = this.tracker.services.GetDeferredSourceFactory(am).CreateDeferredSource(this.current); if (am.StorageAccessor.HasValue(this.current)) { this.AssignMember(this.current, am, ds.Cast().SingleOrDefault()); } } } } } private IEnumerable GetAssociationsForKey(MetaDataMember key) { foreach (MetaDataMember mm in this.type.PersistentDataMembers) { if (mm.IsAssociation && mm.Association.ThisKey.Contains(key)) { yield return mm; } } } internal override object CreateDataCopy(object instance) { System.Diagnostics.Debug.Assert(instance != null); Type instanceType = instance.GetType(); System.Diagnostics.Debug.Assert(instance.GetType() == this.type.Type); object copy = Activator.CreateInstance(this.Type.Type); MetaType rootMetaType = this.tracker.services.Model.GetTable(instanceType).RowType.InheritanceRoot; foreach (MetaDataMember mm in rootMetaType.GetInheritanceType(instanceType).PersistentDataMembers) { if (this.Type.Type != instanceType && !mm.DeclaringType.Type.IsAssignableFrom(instanceType)) { continue; } if (mm.IsDeferred) { // do not copy associations if (!mm.IsAssociation) { if (mm.StorageAccessor.HasValue(instance)) { object value = mm.DeferredValueAccessor.GetBoxedValue(instance); mm.DeferredValueAccessor.SetBoxedValue(ref copy, value); } else { IEnumerable ds = this.tracker.services.GetDeferredSourceFactory(mm).CreateDeferredSource(copy); mm.DeferredSourceAccessor.SetBoxedValue(ref copy, ds); } } } else { // otherwise assign the value as-is to the backup instance object value = mm.StorageAccessor.GetBoxedValue(instance); // assumes member values are immutable or will communicate changes to entity // note: byte[] and char[] don't do this. mm.StorageAccessor.SetBoxedValue(ref copy, value); } } return copy; } internal void StartTracking() { if (this.original == this.current) { this.original = this.CreateDataCopy(this.current); } } // Return value indicates whether or not any data was actually sync'd internal override bool SynchDependentData() { bool valueWasSet = false; // set foreign key fields foreach (MetaAssociation assoc in this.Type.Associations) { MetaDataMember mm = assoc.ThisMember; if (assoc.IsForeignKey) { bool hasAssigned = mm.StorageAccessor.HasAssignedValue(this.current); bool hasLoaded = mm.StorageAccessor.HasLoadedValue(this.current); if (hasAssigned || hasLoaded) { object parent = mm.StorageAccessor.GetBoxedValue(this.current); if (parent != null) { // copy parent's current primary key into this instance's foreign key fields for (int i = 0, n = assoc.ThisKey.Count; i < n; i++) { MetaDataMember accThis = assoc.ThisKey[i]; MetaDataMember accParent = assoc.OtherKey[i]; object parentValue = accParent.StorageAccessor.GetBoxedValue(parent); accThis.StorageAccessor.SetBoxedValue(ref this.current, parentValue); valueWasSet = true; } } else if (assoc.IsNullable) { if (mm.IsDeferred || (this.original != null && mm.MemberAccessor.GetBoxedValue(this.original) != null)) { // no known parent? set to null for (int i = 0, n = assoc.ThisKey.Count; i < n; i++) { MetaDataMember accThis = assoc.ThisKey[i]; if (accThis.CanBeNull) { if (this.original != null && this.HasChangedValue(accThis)) { if (accThis.StorageAccessor.GetBoxedValue(this.current) != null) { throw Error.InconsistentAssociationAndKeyChange(accThis.Member.Name, mm.Member.Name); } } else { accThis.StorageAccessor.SetBoxedValue(ref this.current, null); valueWasSet = true; } } } } } else if (!hasLoaded) { //Else the parent association has been set to null; but the ID is not nullable so //the value can not be set StringBuilder keys = new StringBuilder(); foreach (MetaDataMember key in assoc.ThisKey) { if (keys.Length > 0) { keys.Append(", "); } keys.AppendFormat("{0}.{1}", this.Type.Name.ToString(), key.Name); } throw Error.CouldNotRemoveRelationshipBecauseOneSideCannotBeNull(assoc.OtherType.Name, this.Type.Name, keys); } } } } /// Explicitly set any inheritance discriminator for item. if (this.type.HasInheritance) { if (this.original != null) { object currentDiscriminator = type.Discriminator.MemberAccessor.GetBoxedValue(this.current); MetaType currentTypeFromDiscriminator = TypeFromDiscriminator(this.type, currentDiscriminator); object dbDiscriminator = type.Discriminator.MemberAccessor.GetBoxedValue(this.original); MetaType dbTypeFromDiscriminator = TypeFromDiscriminator(this.type, dbDiscriminator); // Would the discriminator change also change the type? If so, its not allowed. if (currentTypeFromDiscriminator != dbTypeFromDiscriminator) { throw Error.CannotChangeInheritanceType(dbDiscriminator, currentDiscriminator, original.GetType().Name, currentTypeFromDiscriminator); } } else { // No db value means this is an 'Add'. Set the discriminator. MetaType currentType = type.GetInheritanceType(this.current.GetType()); if (currentType.HasInheritanceCode) { object code = currentType.InheritanceCode; this.type.Discriminator.MemberAccessor.SetBoxedValue(ref current, code); valueWasSet = true; } } } return valueWasSet; } internal override bool HasChangedValue(MetaDataMember mm) { if (this.current == this.original) { return false; } if (mm.IsAssociation && mm.Association.IsMany) { return mm.StorageAccessor.HasAssignedValue(this.original); } if (mm.StorageAccessor.HasValue(this.current)) { if (this.original != null && mm.StorageAccessor.HasValue(this.original)) { // If the member has ever been in a modified state // in the past, it is considered modified if (dirtyMemberCache.Get(mm.Ordinal)) { return true; } object baseline = mm.MemberAccessor.GetBoxedValue(this.original); object currentValue = mm.MemberAccessor.GetBoxedValue(this.current); if (!object.Equals(currentValue, baseline)) { return true; } return false; } else if (mm.IsDeferred && mm.StorageAccessor.HasAssignedValue(this.current)) { return true; } } return false; } internal override bool HasChangedValues() { if (this.current == this.original) { return false; } if (this.IsNew) { return true; } foreach (MetaDataMember mm in this.type.PersistentDataMembers) { if (!mm.IsAssociation && this.HasChangedValue(mm)) { return true; } } return false; } internal override IEnumerable GetModifiedMembers() { foreach (MetaDataMember mm in this.type.PersistentDataMembers) { if (this.IsModifiedMember(mm)) { object currentValue = mm.MemberAccessor.GetBoxedValue(this.current); if (this.original != null && mm.StorageAccessor.HasValue(this.original)) { object originalValue = mm.MemberAccessor.GetBoxedValue(this.original); yield return new ModifiedMemberInfo(mm.Member, currentValue, originalValue); } else if (this.original == null || (mm.IsDeferred && !mm.StorageAccessor.HasLoadedValue(this.current))) { yield return new ModifiedMemberInfo(mm.Member, currentValue, null); } } } } private bool IsModifiedMember(MetaDataMember member) { return !member.IsAssociation && !member.IsPrimaryKey && !member.IsVersion && !member.IsDbGenerated && member.StorageAccessor.HasAssignedValue(this.current) && (this.state == State.Modified || (this.state == State.PossiblyModified && this.HasChangedValue(member))); } internal override bool HasDeferredLoaders { get { foreach (MetaAssociation assoc in this.Type.Associations) { if (HasDeferredLoader(assoc.ThisMember)) { return true; } } IEnumerable deferredMembers = this.Type.PersistentDataMembers.Where(p => p.IsDeferred && !p.IsAssociation); foreach (MetaDataMember deferredMember in deferredMembers) { if (HasDeferredLoader(deferredMember)) { return true; } } return false; } } private bool HasDeferredLoader(MetaDataMember deferredMember) { if (!deferredMember.IsDeferred) { return false; } MetaAccessor acc = deferredMember.StorageAccessor; if (acc.HasAssignedValue(this.current) || acc.HasLoadedValue(this.current)) { return false; } MetaAccessor dsacc = deferredMember.DeferredSourceAccessor; IEnumerable loader = (IEnumerable)dsacc.GetBoxedValue(this.current); return loader != null; } /// /// Called to initialize deferred loaders for New or Attached entities. /// internal override void InitializeDeferredLoaders() { if (this.tracker.services.Context.DeferredLoadingEnabled) { foreach (MetaAssociation assoc in this.Type.Associations) { // don't set loader on association that is dependent on unrealized generated values if (!this.IsPendingGeneration(assoc.ThisKey)) { InitializeDeferredLoader(assoc.ThisMember); } } IEnumerable deferredMembers = this.Type.PersistentDataMembers.Where(p => p.IsDeferred && !p.IsAssociation); foreach (MetaDataMember deferredMember in deferredMembers) { // don't set loader on member that is dependent on unrealized generated values if (!this.IsPendingGeneration(Type.IdentityMembers)) { InitializeDeferredLoader(deferredMember); } } haveInitializedDeferredLoaders = true; } } private void InitializeDeferredLoader(MetaDataMember deferredMember) { MetaAccessor acc = deferredMember.StorageAccessor; if (!acc.HasAssignedValue(this.current) && !acc.HasLoadedValue(this.current)) { MetaAccessor dsacc = deferredMember.DeferredSourceAccessor; IEnumerable loader = (IEnumerable)dsacc.GetBoxedValue(this.current); // don't reset loader on any deferred member that already has one if (loader == null) { IDeferredSourceFactory factory = this.tracker.services.GetDeferredSourceFactory(deferredMember); loader = factory.CreateDeferredSource(this.current); dsacc.SetBoxedValue(ref this.current, loader); } else if (loader != null && !haveInitializedDeferredLoaders) { // If loader is present but wasn't generated by us, then // an attempt to Attach or Add an entity from another context // has been made, which is not supported. throw Error.CannotAttachAddNonNewEntities(); } } } internal override bool IsPendingGeneration(IEnumerable key) { if (this.IsNew) { foreach (MetaDataMember member in key) { if (IsMemberPendingGeneration(member)) { return true; } } } return false; } internal override bool IsMemberPendingGeneration(MetaDataMember keyMember) { if (this.IsNew && keyMember.IsDbGenerated) { return true; } // look for any FK association that has this key member (should only be one) foreach (MetaAssociation assoc in type.Associations) { if (assoc.IsForeignKey) { int index = assoc.ThisKey.IndexOf(keyMember); if (index > -1) { // we must have a reference to this other object to know if its side of // the association is generated or not object otherItem = null; if (assoc.ThisMember.IsDeferred) { otherItem = assoc.ThisMember.DeferredValueAccessor.GetBoxedValue(this.current); } else { otherItem = assoc.ThisMember.StorageAccessor.GetBoxedValue(this.current); } if (otherItem != null) { if (assoc.IsMany) { // Can't be pending generation for a value that would have to be the same // across many rows. continue; } else { StandardTrackedObject trackedOther = (StandardTrackedObject)this.tracker.GetTrackedObject(otherItem); if (trackedOther != null) { MetaDataMember otherMember = assoc.OtherKey[index]; return trackedOther.IsMemberPendingGeneration(otherMember); } } } } } } return false; } } } /// /// This is the implementation used when change tracking is disabled. /// class ReadOnlyChangeTracker : ChangeTracker { internal override TrackedObject Track(object obj) { return null; } internal override TrackedObject Track(object obj, bool recurse) { return null; } internal override void FastTrack(object obj) { } internal override bool IsTracked(object obj) { return false; } internal override TrackedObject GetTrackedObject(object obj) { return null; } internal override void StopTracking(object obj) { } internal override void AcceptChanges() { } internal override IEnumerable GetInterestingObjects() { return new TrackedObject[0]; } } } internal abstract class TrackedObject { internal abstract MetaType Type { get; } /// /// The current client value. /// internal abstract object Current { get; } /// /// The last read database value. This is updated whenever the /// item is refreshed. /// internal abstract object Original { get; } internal abstract bool IsInteresting { get; } // new, deleted or possibly changed internal abstract bool IsNew { get; } internal abstract bool IsDeleted { get; } internal abstract bool IsModified { get; } internal abstract bool IsUnmodified { get; } internal abstract bool IsPossiblyModified { get; } internal abstract bool IsRemoved { get; } internal abstract bool IsDead { get; } /// /// True if the object is being tracked (perhaps during a recursive /// attach operation) but can be transitioned to other states. /// internal abstract bool IsWeaklyTracked { get; } internal abstract bool HasDeferredLoaders { get; } internal abstract bool HasChangedValues(); internal abstract IEnumerable GetModifiedMembers(); internal abstract bool HasChangedValue(MetaDataMember mm); internal abstract bool CanInferDelete(); internal abstract void AcceptChanges(); internal abstract void ConvertToNew(); internal abstract void ConvertToPossiblyModified(); internal abstract void ConvertToPossiblyModified(object original); internal abstract void ConvertToUnmodified(); internal abstract void ConvertToModified(); internal abstract void ConvertToDeleted(); internal abstract void ConvertToRemoved(); internal abstract void ConvertToDead(); /// /// Refresh the item by making the value passed in the current /// Database value, and refreshing the current values using the /// mode specified. /// internal abstract void Refresh(RefreshMode mode, object freshInstance); /// /// Does the refresh operation for a single member. This method does not /// update the baseline 'original' value. You must call /// Refresh(RefreshMode.KeepCurrentValues, freshInstance) to finish the refresh /// after refreshing individual members. /// /// /// /// internal abstract void RefreshMember(MetaDataMember member, RefreshMode mode, object freshValue); /// /// Create a data-member only copy of the instance (no associations) /// /// internal abstract object CreateDataCopy(object instance); internal abstract bool SynchDependentData(); internal abstract bool IsPendingGeneration(IEnumerable keyMembers); internal abstract bool IsMemberPendingGeneration(MetaDataMember keyMember); internal abstract void InitializeDeferredLoaders(); } }