2101 lines
75 KiB
C#
2101 lines
75 KiB
C#
using LeanCloud.Storage.Internal;
|
|
using LeanCloud.Utilities;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Collections;
|
|
|
|
namespace LeanCloud
|
|
{
|
|
/// <summary>
|
|
/// The AVObject is a local representation of data that can be saved and
|
|
/// retrieved from the LeanCloud cloud.</summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The basic workflow for creating new data is to construct a new AVObject,
|
|
/// use the indexer to fill it with data, and then use SaveAsync() to persist to the
|
|
/// database.
|
|
/// </para>
|
|
/// <para>
|
|
/// The basic workflow for accessing existing data is to use a AVQuery
|
|
/// to specify which existing data to retrieve.
|
|
/// </para>
|
|
/// </remarks>
|
|
public class AVObject : IEnumerable<KeyValuePair<string, object>>, INotifyPropertyChanged, INotifyPropertyUpdated, INotifyCollectionPropertyUpdated, IAVObject
|
|
{
|
|
private static readonly string AutoClassName = "_Automatic";
|
|
|
|
#if UNITY
|
|
private static readonly bool isCompiledByIL2CPP = AppDomain.CurrentDomain.FriendlyName.Equals("IL2CPP Root Domain");
|
|
#else
|
|
private static readonly bool isCompiledByIL2CPP = false;
|
|
#endif
|
|
|
|
|
|
internal readonly object mutex = new object();
|
|
|
|
private readonly LinkedList<IDictionary<string, IAVFieldOperation>> operationSetQueue =
|
|
new LinkedList<IDictionary<string, IAVFieldOperation>>();
|
|
private readonly IDictionary<string, object> estimatedData = new Dictionary<string, object>();
|
|
|
|
private static readonly ThreadLocal<bool> isCreatingPointer = new ThreadLocal<bool>(() => false);
|
|
|
|
private bool hasBeenFetched;
|
|
private bool dirty;
|
|
internal TaskQueue taskQueue = new TaskQueue();
|
|
|
|
private IObjectState state;
|
|
internal void MutateState(Action<MutableObjectState> func)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
state = state.MutatedClone(func);
|
|
|
|
// Refresh the estimated data.
|
|
RebuildEstimatedData();
|
|
}
|
|
}
|
|
|
|
public IObjectState State
|
|
{
|
|
get
|
|
{
|
|
return state;
|
|
}
|
|
}
|
|
|
|
internal static IAVObjectController ObjectController
|
|
{
|
|
get
|
|
{
|
|
return AVPlugins.Instance.ObjectController;
|
|
}
|
|
}
|
|
|
|
internal static IObjectSubclassingController SubclassingController
|
|
{
|
|
get
|
|
{
|
|
return AVPlugins.Instance.SubclassingController;
|
|
}
|
|
}
|
|
|
|
public static string GetSubClassName<TAVObject>()
|
|
{
|
|
return SubclassingController.GetClassName(typeof(TAVObject));
|
|
}
|
|
|
|
#region AVObject Creation
|
|
|
|
/// <summary>
|
|
/// Constructor for use in AVObject subclasses. Subclasses must specify a AVClassName attribute.
|
|
/// </summary>
|
|
protected AVObject()
|
|
: this(AutoClassName)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructs a new AVObject with no data in it. A AVObject constructed in this way will
|
|
/// not have an ObjectId and will not persist to the database until <see cref="SaveAsync()"/>
|
|
/// is called.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Class names must be alphanumerical plus underscore, and start with a letter. It is recommended
|
|
/// to name classes in CamelCaseLikeThis.
|
|
/// </remarks>
|
|
/// <param name="className">The className for this AVObject.</param>
|
|
public AVObject(string className)
|
|
{
|
|
// We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the
|
|
// right thing with subclasses. It's ugly and terrible, but it does provide the development
|
|
// experience we generally want, so... yeah. Sorry to whomever has to deal with this in the
|
|
// future. I pinky-swear we won't make a habit of this -- you believe me, don't you?
|
|
var isPointer = isCreatingPointer.Value;
|
|
isCreatingPointer.Value = false;
|
|
|
|
if (className == null)
|
|
{
|
|
throw new ArgumentException("You must specify a LeanCloud class name when creating a new AVObject.");
|
|
}
|
|
if (AutoClassName.Equals(className))
|
|
{
|
|
className = SubclassingController.GetClassName(GetType());
|
|
}
|
|
// If this is supposed to be created by a factory but wasn't, throw an exception
|
|
if (!SubclassingController.IsTypeValid(className, GetType()))
|
|
{
|
|
throw new ArgumentException(
|
|
"You must create this type of AVObject using AVObject.Create() or the proper subclass.");
|
|
}
|
|
state = new MutableObjectState
|
|
{
|
|
ClassName = className
|
|
};
|
|
OnPropertyChanged("ClassName");
|
|
|
|
operationSetQueue.AddLast(new Dictionary<string, IAVFieldOperation>());
|
|
if (!isPointer)
|
|
{
|
|
hasBeenFetched = true;
|
|
IsDirty = true;
|
|
SetDefaultValues();
|
|
}
|
|
else
|
|
{
|
|
IsDirty = false;
|
|
hasBeenFetched = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new AVObject based upon a class name. If the class name is a special type (e.g.
|
|
/// for <see cref="AVUser"/>), then the appropriate type of AVObject is returned.
|
|
/// </summary>
|
|
/// <param name="className">The class of object to create.</param>
|
|
/// <returns>A new AVObject for the given class name.</returns>
|
|
public static AVObject Create(string className)
|
|
{
|
|
return SubclassingController.Instantiate(className);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a reference to an existing AVObject for use in creating associations between
|
|
/// AVObjects. Calling <see cref="AVObject.IsDataAvailable"/> on this object will return
|
|
/// <c>false</c> until <see cref="AVExtensions.FetchIfNeededAsync{T}(T)"/> has been called.
|
|
/// No network request will be made.
|
|
/// </summary>
|
|
/// <param name="className">The object's class.</param>
|
|
/// <param name="objectId">The object id for the referenced object.</param>
|
|
/// <returns>A AVObject without data.</returns>
|
|
public static AVObject CreateWithoutData(string className, string objectId)
|
|
{
|
|
isCreatingPointer.Value = true;
|
|
try
|
|
{
|
|
var result = SubclassingController.Instantiate(className);
|
|
result.ObjectId = objectId;
|
|
result.IsDirty = false;
|
|
if (result.IsDirty)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A AVObject subclass default constructor must not make changes to the object that cause it to be dirty.");
|
|
}
|
|
return result;
|
|
}
|
|
finally
|
|
{
|
|
isCreatingPointer.Value = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new AVObject based upon a given subclass type.
|
|
/// </summary>
|
|
/// <returns>A new AVObject for the given class name.</returns>
|
|
public static T Create<T>() where T : AVObject
|
|
{
|
|
return (T)SubclassingController.Instantiate(SubclassingController.GetClassName(typeof(T)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a reference to an existing AVObject for use in creating associations between
|
|
/// AVObjects. Calling <see cref="AVObject.IsDataAvailable"/> on this object will return
|
|
/// <c>false</c> until <see cref="AVExtensions.FetchIfNeededAsync{T}(T)"/> has been called.
|
|
/// No network request will be made.
|
|
/// </summary>
|
|
/// <param name="objectId">The object id for the referenced object.</param>
|
|
/// <returns>A AVObject without data.</returns>
|
|
public static T CreateWithoutData<T>(string objectId) where T : AVObject
|
|
{
|
|
return (T)CreateWithoutData(SubclassingController.GetClassName(typeof(T)), objectId);
|
|
}
|
|
|
|
///<summary>
|
|
/// restore a AVObject of subclass instance from IObjectState.
|
|
/// </summary>
|
|
/// <param name="state">IObjectState after encode from Dictionary.</param>
|
|
/// <param name="defaultClassName">The name of the subclass.</param>
|
|
public static T FromState<T>(IObjectState state, string defaultClassName) where T : AVObject
|
|
{
|
|
string className = state.ClassName ?? defaultClassName;
|
|
|
|
T obj = (T)CreateWithoutData(className, state.ObjectId);
|
|
obj.HandleFetchResult(state);
|
|
|
|
return obj;
|
|
}
|
|
|
|
#endregion
|
|
|
|
public static IDictionary<String, String> GetPropertyMappings(string className)
|
|
{
|
|
return SubclassingController.GetPropertyMappings(className);
|
|
}
|
|
|
|
private static string GetFieldForPropertyName(string className, string propertyName)
|
|
{
|
|
String fieldName = null;
|
|
SubclassingController.GetPropertyMappings(className).TryGetValue(propertyName, out fieldName);
|
|
return fieldName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the value of a property based upon its associated AVFieldName attribute.
|
|
/// </summary>
|
|
/// <param name="value">The new value.</param>
|
|
/// <param name="propertyName">The name of the property.</param>
|
|
/// <typeparam name="T">The type for the property.</typeparam>
|
|
protected virtual void SetProperty<T>(T value,
|
|
#if !UNITY
|
|
[CallerMemberName] string propertyName = null
|
|
#else
|
|
string propertyName
|
|
#endif
|
|
)
|
|
{
|
|
this[GetFieldForPropertyName(ClassName, propertyName)] = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a relation for a property based upon its associated AVFieldName attribute.
|
|
/// </summary>
|
|
/// <returns>The AVRelation for the property.</returns>
|
|
/// <param name="propertyName">The name of the property.</param>
|
|
/// <typeparam name="T">The AVObject subclass type of the AVRelation.</typeparam>
|
|
protected AVRelation<T> GetRelationProperty<T>(
|
|
#if !UNITY
|
|
[CallerMemberName] string propertyName = null
|
|
#else
|
|
string propertyName
|
|
#endif
|
|
) where T : AVObject
|
|
{
|
|
return GetRelation<T>(GetFieldForPropertyName(ClassName, propertyName));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of a property based upon its associated AVFieldName attribute.
|
|
/// </summary>
|
|
/// <returns>The value of the property.</returns>
|
|
/// <param name="propertyName">The name of the property.</param>
|
|
/// <typeparam name="T">The return type of the property.</typeparam>
|
|
protected virtual T GetProperty<T>(
|
|
#if !UNITY
|
|
[CallerMemberName] string propertyName = null
|
|
#else
|
|
string propertyName
|
|
#endif
|
|
)
|
|
{
|
|
return GetProperty<T>(default(T), propertyName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the value of a property based upon its associated AVFieldName attribute.
|
|
/// </summary>
|
|
/// <returns>The value of the property.</returns>
|
|
/// <param name="defaultValue">The value to return if the property is not present on the AVObject.</param>
|
|
/// <param name="propertyName">The name of the property.</param>
|
|
/// <typeparam name="T">The return type of the property.</typeparam>
|
|
protected virtual T GetProperty<T>(T defaultValue,
|
|
#if !UNITY
|
|
[CallerMemberName] string propertyName = null
|
|
#else
|
|
string propertyName
|
|
#endif
|
|
)
|
|
{
|
|
T result;
|
|
if (TryGetValue<T>(GetFieldForPropertyName(ClassName, propertyName), out result))
|
|
{
|
|
return result;
|
|
}
|
|
return defaultValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows subclasses to set values for non-pointer construction.
|
|
/// </summary>
|
|
internal virtual void SetDefaultValues()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a custom subclass type with the LeanCloud SDK, enabling strong-typing of those AVObjects whenever
|
|
/// they appear. Subclasses must specify the AVClassName attribute, have a default constructor, and properties
|
|
/// backed by AVObject fields should have AVFieldName attributes supplied.
|
|
/// </summary>
|
|
/// <typeparam name="T">The AVObject subclass type to register.</typeparam>
|
|
public static void RegisterSubclass<T>() where T : AVObject, new()
|
|
{
|
|
SubclassingController.RegisterSubclass(typeof(T));
|
|
}
|
|
|
|
internal static void UnregisterSubclass<T>() where T : AVObject, new()
|
|
{
|
|
SubclassingController.UnregisterSubclass(typeof(T));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears any changes to this object made since the last call to <see cref="SaveAsync()"/>.
|
|
/// </summary>
|
|
public void Revert()
|
|
{
|
|
lock (mutex)
|
|
{
|
|
bool wasDirty = CurrentOperations.Count > 0;
|
|
if (wasDirty)
|
|
{
|
|
CurrentOperations.Clear();
|
|
RebuildEstimatedData();
|
|
OnPropertyChanged("IsDirty");
|
|
}
|
|
}
|
|
}
|
|
|
|
internal virtual void HandleFetchResult(IObjectState serverState)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
MergeFromServer(serverState);
|
|
}
|
|
}
|
|
|
|
internal void HandleFailedSave(
|
|
IDictionary<string, IAVFieldOperation> operationsBeforeSave)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
var opNode = operationSetQueue.Find(operationsBeforeSave);
|
|
var nextOperations = opNode.Next.Value;
|
|
bool wasDirty = nextOperations.Count > 0;
|
|
operationSetQueue.Remove(opNode);
|
|
// Merge the data from the failed save into the next save.
|
|
foreach (var pair in operationsBeforeSave)
|
|
{
|
|
var operation1 = pair.Value;
|
|
IAVFieldOperation operation2 = null;
|
|
nextOperations.TryGetValue(pair.Key, out operation2);
|
|
if (operation2 != null)
|
|
{
|
|
operation2 = operation2.MergeWithPrevious(operation1);
|
|
}
|
|
else
|
|
{
|
|
operation2 = operation1;
|
|
}
|
|
nextOperations[pair.Key] = operation2;
|
|
}
|
|
if (!wasDirty && nextOperations == CurrentOperations && operationsBeforeSave.Count > 0)
|
|
{
|
|
OnPropertyChanged("IsDirty");
|
|
}
|
|
}
|
|
}
|
|
|
|
internal virtual void HandleSave(IObjectState serverState)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
var operationsBeforeSave = operationSetQueue.First.Value;
|
|
operationSetQueue.RemoveFirst();
|
|
|
|
// Merge the data from the save and the data from the server into serverData.
|
|
//MutateState(mutableClone =>
|
|
//{
|
|
// mutableClone.Apply(operationsBeforeSave);
|
|
//});
|
|
state = state.MutatedClone((objectState) => objectState.Apply(operationsBeforeSave));
|
|
MergeFromServer(serverState);
|
|
}
|
|
}
|
|
|
|
public virtual void MergeFromServer(IObjectState serverState)
|
|
{
|
|
// Make a new serverData with fetched values.
|
|
var newServerData = serverState.ToDictionary(t => t.Key, t => t.Value);
|
|
|
|
lock (mutex)
|
|
{
|
|
// Trigger handler based on serverState
|
|
if (serverState.ObjectId != null)
|
|
{
|
|
// If the objectId is being merged in, consider this object to be fetched.
|
|
hasBeenFetched = true;
|
|
OnPropertyChanged("IsDataAvailable");
|
|
}
|
|
|
|
if (serverState.UpdatedAt != null)
|
|
{
|
|
OnPropertyChanged("UpdatedAt");
|
|
}
|
|
|
|
if (serverState.CreatedAt != null)
|
|
{
|
|
OnPropertyChanged("CreatedAt");
|
|
}
|
|
|
|
// We cache the fetched object because subsequent Save operation might flush
|
|
// the fetched objects into Pointers.
|
|
IDictionary<string, AVObject> fetchedObject = CollectFetchedObjects();
|
|
|
|
foreach (var pair in serverState)
|
|
{
|
|
var value = pair.Value;
|
|
if (value is AVObject)
|
|
{
|
|
// Resolve fetched object.
|
|
var avObject = value as AVObject;
|
|
if (fetchedObject.ContainsKey(avObject.ObjectId))
|
|
{
|
|
value = fetchedObject[avObject.ObjectId];
|
|
}
|
|
}
|
|
newServerData[pair.Key] = value;
|
|
}
|
|
|
|
IsDirty = false;
|
|
serverState = serverState.MutatedClone(mutableClone =>
|
|
{
|
|
mutableClone.ServerData = newServerData;
|
|
});
|
|
MutateState(mutableClone =>
|
|
{
|
|
mutableClone.Apply(serverState);
|
|
});
|
|
}
|
|
}
|
|
|
|
internal void MergeFromObject(AVObject other)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
// If they point to the same instance, we don't need to merge
|
|
if (this == other)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Clear out any changes on this object.
|
|
if (operationSetQueue.Count != 1)
|
|
{
|
|
throw new InvalidOperationException("Attempt to MergeFromObject during save.");
|
|
}
|
|
operationSetQueue.Clear();
|
|
foreach (var operationSet in other.operationSetQueue)
|
|
{
|
|
operationSetQueue.AddLast(operationSet.ToDictionary(entry => entry.Key,
|
|
entry => entry.Value));
|
|
}
|
|
|
|
lock (mutex)
|
|
{
|
|
state = other.State;
|
|
}
|
|
RebuildEstimatedData();
|
|
}
|
|
|
|
private bool HasDirtyChildren
|
|
{
|
|
get
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return FindUnsavedChildren().FirstOrDefault() != null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Flattens dictionaries and lists into a single enumerable of all contained objects
|
|
/// that can then be queried over.
|
|
/// </summary>
|
|
/// <param name="root">The root of the traversal</param>
|
|
/// <param name="traverseAVObjects">Whether to traverse into AVObjects' children</param>
|
|
/// <param name="yieldRoot">Whether to include the root in the result</param>
|
|
/// <returns></returns>
|
|
internal static IEnumerable<object> DeepTraversal(
|
|
object root, bool traverseAVObjects = false, bool yieldRoot = false)
|
|
{
|
|
var items = DeepTraversalInternal(root,
|
|
traverseAVObjects,
|
|
new HashSet<object>(new IdentityEqualityComparer<object>()));
|
|
if (yieldRoot)
|
|
{
|
|
return new[] { root }.Concat(items);
|
|
}
|
|
else
|
|
{
|
|
return items;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<object> DeepTraversalInternal(
|
|
object root, bool traverseAVObjects, ICollection<object> seen)
|
|
{
|
|
seen.Add(root);
|
|
var itemsToVisit = isCompiledByIL2CPP ? (System.Collections.IEnumerable)null : (IEnumerable<object>)null;
|
|
var dict = Conversion.As<IDictionary<string, object>>(root);
|
|
if (dict != null)
|
|
{
|
|
itemsToVisit = dict.Values;
|
|
}
|
|
else
|
|
{
|
|
var list = Conversion.As<IList<object>>(root);
|
|
if (list != null)
|
|
{
|
|
itemsToVisit = list;
|
|
}
|
|
else if (traverseAVObjects)
|
|
{
|
|
var obj = root as AVObject;
|
|
if (obj != null)
|
|
{
|
|
itemsToVisit = obj.Keys.ToList().Select(k => obj[k]);
|
|
}
|
|
}
|
|
}
|
|
if (itemsToVisit != null)
|
|
{
|
|
foreach (var i in itemsToVisit)
|
|
{
|
|
if (!seen.Contains(i))
|
|
{
|
|
yield return i;
|
|
var children = DeepTraversalInternal(i, traverseAVObjects, seen);
|
|
foreach (var child in children)
|
|
{
|
|
yield return child;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerable<AVObject> FindUnsavedChildren()
|
|
{
|
|
return DeepTraversal(estimatedData)
|
|
.OfType<AVObject>()
|
|
.Where(o => o.IsDirty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deep traversal of this object to grab a copy of any object referenced by this object.
|
|
/// These instances may have already been fetched, and we don't want to lose their data when
|
|
/// refreshing or saving.
|
|
/// </summary>
|
|
/// <returns>Map of objectId to AVObject which have been fetched.</returns>
|
|
private IDictionary<string, AVObject> CollectFetchedObjects()
|
|
{
|
|
return DeepTraversal(estimatedData)
|
|
.OfType<AVObject>()
|
|
.Where(o => o.ObjectId != null && o.IsDataAvailable)
|
|
.GroupBy(o => o.ObjectId)
|
|
.ToDictionary(group => group.Key, group => group.Last());
|
|
}
|
|
|
|
public static IDictionary<string, object> ToJSONObjectForSaving(
|
|
IDictionary<string, IAVFieldOperation> operations)
|
|
{
|
|
var result = new Dictionary<string, object>();
|
|
foreach (var pair in operations)
|
|
{
|
|
// AVRPCSerialize the data
|
|
var operation = pair.Value;
|
|
|
|
result[pair.Key] = PointerOrLocalIdEncoder.Instance.Encode(operation);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
internal IDictionary<string, object> EncodeForSaving(IDictionary<string, object> data)
|
|
{
|
|
var result = new Dictionary<string, object>();
|
|
lock (this.mutex)
|
|
{
|
|
foreach (var key in data.Keys)
|
|
{
|
|
var value = data[key];
|
|
result.Add(key, PointerOrLocalIdEncoder.Instance.Encode(value));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
internal IDictionary<string, object> ServerDataToJSONObjectForSerialization()
|
|
{
|
|
return PointerOrLocalIdEncoder.Instance.Encode(state.ToDictionary(t => t.Key, t => t.Value))
|
|
as IDictionary<string, object>;
|
|
}
|
|
|
|
#region Save Object(s)
|
|
|
|
/// <summary>
|
|
/// Pushes new operations onto the queue and returns the current set of operations.
|
|
/// </summary>
|
|
public IDictionary<string, IAVFieldOperation> StartSave()
|
|
{
|
|
lock (mutex)
|
|
{
|
|
var currentOperations = CurrentOperations;
|
|
operationSetQueue.AddLast(new Dictionary<string, IAVFieldOperation>());
|
|
OnPropertyChanged("IsDirty");
|
|
return currentOperations;
|
|
}
|
|
}
|
|
|
|
protected virtual Task SaveAsync(Task toAwait,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
IDictionary<string, IAVFieldOperation> currentOperations = null;
|
|
if (!IsDirty)
|
|
{
|
|
return Task.FromResult(0);
|
|
}
|
|
|
|
Task deepSaveTask;
|
|
string sessionToken;
|
|
lock (mutex)
|
|
{
|
|
// Get the JSON representation of the object.
|
|
currentOperations = StartSave();
|
|
|
|
sessionToken = AVUser.CurrentSessionToken;
|
|
|
|
deepSaveTask = DeepSaveAsync(estimatedData, sessionToken, cancellationToken);
|
|
}
|
|
|
|
return deepSaveTask.OnSuccess(_ =>
|
|
{
|
|
return toAwait;
|
|
}).Unwrap().OnSuccess(_ =>
|
|
{
|
|
return ObjectController.SaveAsync(state,
|
|
currentOperations,
|
|
sessionToken,
|
|
cancellationToken);
|
|
}).Unwrap().ContinueWith(t =>
|
|
{
|
|
if (t.IsFaulted || t.IsCanceled)
|
|
{
|
|
HandleFailedSave(currentOperations);
|
|
}
|
|
else
|
|
{
|
|
var serverState = t.Result;
|
|
HandleSave(serverState);
|
|
}
|
|
return t;
|
|
}).Unwrap();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves this object to the server.
|
|
/// </summary>
|
|
public virtual Task SaveAsync()
|
|
{
|
|
return SaveAsync(CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves this object to the server.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
public virtual Task SaveAsync(CancellationToken cancellationToken)
|
|
{
|
|
return taskQueue.Enqueue(toAwait => SaveAsync(toAwait, cancellationToken),
|
|
cancellationToken);
|
|
}
|
|
|
|
internal virtual Task<AVObject> FetchAsyncInternal(
|
|
Task toAwait,
|
|
IDictionary<string, object> queryString,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
return toAwait.OnSuccess(_ =>
|
|
{
|
|
if (ObjectId == null)
|
|
{
|
|
throw new InvalidOperationException("Cannot refresh an object that hasn't been saved to the server.");
|
|
}
|
|
if (queryString == null)
|
|
{
|
|
queryString = new Dictionary<string, object>();
|
|
}
|
|
|
|
return ObjectController.FetchAsync(state, queryString, AVUser.CurrentSessionToken, cancellationToken);
|
|
}).Unwrap().OnSuccess(t =>
|
|
{
|
|
HandleFetchResult(t.Result);
|
|
return this;
|
|
});
|
|
}
|
|
|
|
private static Task DeepSaveAsync(object obj, string sessionToken, CancellationToken cancellationToken)
|
|
{
|
|
var objects = new List<AVObject>();
|
|
CollectDirtyChildren(obj, objects);
|
|
|
|
var uniqueObjects = new HashSet<AVObject>(objects,
|
|
new IdentityEqualityComparer<AVObject>());
|
|
|
|
var saveDirtyFileTasks = DeepTraversal(obj, true)
|
|
.OfType<AVFile>()
|
|
.Where(f => f.IsDirty)
|
|
.Select(f => f.SaveAsync(cancellationToken)).ToList();
|
|
|
|
return Task.WhenAll(saveDirtyFileTasks).OnSuccess(_ =>
|
|
{
|
|
IEnumerable<AVObject> remaining = new List<AVObject>(uniqueObjects);
|
|
return InternalExtensions.WhileAsync(() => Task.FromResult(remaining.Any()), () =>
|
|
{
|
|
// Partition the objects into two sets: those that can be saved immediately,
|
|
// and those that rely on other objects to be created first.
|
|
var current = (from item in remaining
|
|
where item.CanBeSerialized
|
|
select item).ToList();
|
|
var nextBatch = (from item in remaining
|
|
where !item.CanBeSerialized
|
|
select item).ToList();
|
|
remaining = nextBatch;
|
|
|
|
if (current.Count == 0)
|
|
{
|
|
// We do cycle-detection when building the list of objects passed to this
|
|
// function, so this should never get called. But we should check for it
|
|
// anyway, so that we get an exception instead of an infinite loop.
|
|
throw new InvalidOperationException(
|
|
"Unable to save a AVObject with a relation to a cycle.");
|
|
}
|
|
|
|
// Save all of the objects in current.
|
|
return AVObject.EnqueueForAll<object>(current, toAwait =>
|
|
{
|
|
return toAwait.OnSuccess(__ =>
|
|
{
|
|
var states = (from item in current
|
|
select item.state).ToList();
|
|
var operationsList = (from item in current
|
|
select item.StartSave()).ToList();
|
|
|
|
var saveTasks = ObjectController.SaveAllAsync(states,
|
|
operationsList,
|
|
sessionToken,
|
|
cancellationToken);
|
|
|
|
return Task.WhenAll(saveTasks).ContinueWith(t =>
|
|
{
|
|
if (t.IsFaulted || t.IsCanceled)
|
|
{
|
|
foreach (var pair in current.Zip(operationsList, (item, ops) => new { item, ops }))
|
|
{
|
|
pair.item.HandleFailedSave(pair.ops);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var serverStates = t.Result;
|
|
foreach (var pair in current.Zip(serverStates, (item, state) => new { item, state }))
|
|
{
|
|
pair.item.HandleSave(pair.state);
|
|
}
|
|
}
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
return t;
|
|
}).Unwrap();
|
|
}).Unwrap().OnSuccess(t => (object)null);
|
|
}, cancellationToken);
|
|
});
|
|
}).Unwrap();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves each object in the provided list.
|
|
/// </summary>
|
|
/// <param name="objects">The objects to save.</param>
|
|
public static Task SaveAllAsync<T>(IEnumerable<T> objects) where T : AVObject
|
|
{
|
|
return SaveAllAsync(objects, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves each object in the provided list.
|
|
/// </summary>
|
|
/// <param name="objects">The objects to save.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
public static Task SaveAllAsync<T>(
|
|
IEnumerable<T> objects, CancellationToken cancellationToken) where T : AVObject
|
|
{
|
|
return DeepSaveAsync(objects.ToList(), AVUser.CurrentSessionToken, cancellationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Fetch Object(s)
|
|
|
|
/// <summary>
|
|
/// Fetches this object with the data from the server.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
internal Task<AVObject> FetchAsyncInternal(CancellationToken cancellationToken)
|
|
{
|
|
return FetchAsyncInternal(null, cancellationToken);
|
|
}
|
|
|
|
internal Task<AVObject> FetchAsyncInternal(IDictionary<string, object> queryString, CancellationToken cancellationToken)
|
|
{
|
|
return taskQueue.Enqueue(toAwait => FetchAsyncInternal(toAwait, queryString, cancellationToken),
|
|
cancellationToken);
|
|
}
|
|
|
|
internal Task<AVObject> FetchIfNeededAsyncInternal(
|
|
Task toAwait, CancellationToken cancellationToken)
|
|
{
|
|
if (!IsDataAvailable)
|
|
{
|
|
return FetchAsyncInternal(toAwait, null, cancellationToken);
|
|
}
|
|
return Task.FromResult(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// If this AVObject has not been fetched (i.e. <see cref="IsDataAvailable"/> returns
|
|
/// false), fetches this object with the data from the server.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
internal Task<AVObject> FetchIfNeededAsyncInternal(CancellationToken cancellationToken)
|
|
{
|
|
return taskQueue.Enqueue(toAwait => FetchIfNeededAsyncInternal(toAwait, cancellationToken),
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches all of the objects that don't have data in the provided list.
|
|
/// </summary>
|
|
/// <returns>The list passed in for convenience.</returns>
|
|
public static Task<IEnumerable<T>> FetchAllIfNeededAsync<T>(
|
|
IEnumerable<T> objects) where T : AVObject
|
|
{
|
|
return FetchAllIfNeededAsync(objects, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches all of the objects that don't have data in the provided list.
|
|
/// </summary>
|
|
/// <param name="objects">The objects to fetch.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The list passed in for convenience.</returns>
|
|
public static Task<IEnumerable<T>> FetchAllIfNeededAsync<T>(
|
|
IEnumerable<T> objects, CancellationToken cancellationToken) where T : AVObject
|
|
{
|
|
return AVObject.EnqueueForAll(objects.Cast<AVObject>(), (Task toAwait) =>
|
|
{
|
|
return FetchAllInternalAsync(objects, false, toAwait, cancellationToken);
|
|
}, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches all of the objects in the provided list.
|
|
/// </summary>
|
|
/// <param name="objects">The objects to fetch.</param>
|
|
/// <returns>The list passed in for convenience.</returns>
|
|
public static Task<IEnumerable<T>> FetchAllAsync<T>(
|
|
IEnumerable<T> objects) where T : AVObject
|
|
{
|
|
return FetchAllAsync(objects, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches all of the objects in the provided list.
|
|
/// </summary>
|
|
/// <param name="objects">The objects to fetch.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The list passed in for convenience.</returns>
|
|
public static Task<IEnumerable<T>> FetchAllAsync<T>(
|
|
IEnumerable<T> objects, CancellationToken cancellationToken) where T : AVObject
|
|
{
|
|
return AVObject.EnqueueForAll(objects.Cast<AVObject>(), (Task toAwait) =>
|
|
{
|
|
return FetchAllInternalAsync(objects, true, toAwait, cancellationToken);
|
|
}, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetches all of the objects in the list.
|
|
/// </summary>
|
|
/// <param name="objects">The objects to fetch.</param>
|
|
/// <param name="force">If false, only objects without data will be fetched.</param>
|
|
/// <param name="toAwait">A task to await before starting.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The list passed in for convenience.</returns>
|
|
private static Task<IEnumerable<T>> FetchAllInternalAsync<T>(
|
|
IEnumerable<T> objects, bool force, Task toAwait, CancellationToken cancellationToken) where T : AVObject
|
|
{
|
|
return toAwait.OnSuccess(_ =>
|
|
{
|
|
if (objects.Any(obj => { return obj.state.ObjectId == null; }))
|
|
{
|
|
throw new InvalidOperationException("You cannot fetch objects that haven't already been saved.");
|
|
}
|
|
|
|
var objectsToFetch = (from obj in objects
|
|
where force || !obj.IsDataAvailable
|
|
select obj).ToList();
|
|
|
|
if (objectsToFetch.Count == 0)
|
|
{
|
|
return Task.FromResult(objects);
|
|
}
|
|
|
|
// Do one Find for each class.
|
|
var findsByClass =
|
|
(from obj in objectsToFetch
|
|
group obj.ObjectId by obj.ClassName into classGroup
|
|
where classGroup.Count() > 0
|
|
select new
|
|
{
|
|
ClassName = classGroup.Key,
|
|
FindTask = new AVQuery<AVObject>(classGroup.Key)
|
|
.WhereContainedIn("objectId", classGroup)
|
|
.FindAsync(cancellationToken)
|
|
}).ToDictionary(pair => pair.ClassName, pair => pair.FindTask);
|
|
|
|
// Wait for all the Finds to complete.
|
|
return Task.WhenAll(findsByClass.Values.ToList()).OnSuccess(__ =>
|
|
{
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
return objects;
|
|
}
|
|
|
|
// Merge the data from the Finds into the input objects.
|
|
var pairs = from obj in objectsToFetch
|
|
from result in findsByClass[obj.ClassName].Result
|
|
where result.ObjectId == obj.ObjectId
|
|
select new { obj, result };
|
|
foreach (var pair in pairs)
|
|
{
|
|
pair.obj.MergeFromObject(pair.result);
|
|
pair.obj.hasBeenFetched = true;
|
|
}
|
|
|
|
return objects;
|
|
});
|
|
}).Unwrap();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Delete Object
|
|
|
|
internal Task DeleteAsync(Task toAwait, CancellationToken cancellationToken)
|
|
{
|
|
if (ObjectId == null)
|
|
{
|
|
return Task.FromResult(0);
|
|
}
|
|
|
|
string sessionToken = AVUser.CurrentSessionToken;
|
|
|
|
return toAwait.OnSuccess(_ =>
|
|
{
|
|
return ObjectController.DeleteAsync(State, sessionToken, cancellationToken);
|
|
}).Unwrap().OnSuccess(_ => IsDirty = true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes this object on the server.
|
|
/// </summary>
|
|
public Task DeleteAsync()
|
|
{
|
|
return DeleteAsync(CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes this object on the server.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
public Task DeleteAsync(CancellationToken cancellationToken)
|
|
{
|
|
return taskQueue.Enqueue(toAwait => DeleteAsync(toAwait, cancellationToken),
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes each object in the provided list.
|
|
/// </summary>
|
|
/// <param name="objects">The objects to delete.</param>
|
|
public static Task DeleteAllAsync<T>(IEnumerable<T> objects) where T : AVObject
|
|
{
|
|
return DeleteAllAsync(objects, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes each object in the provided list.
|
|
/// </summary>
|
|
/// <param name="objects">The objects to delete.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
public static Task DeleteAllAsync<T>(
|
|
IEnumerable<T> objects, CancellationToken cancellationToken) where T : AVObject
|
|
{
|
|
var uniqueObjects = new HashSet<AVObject>(objects.OfType<AVObject>().ToList(),
|
|
new IdentityEqualityComparer<AVObject>());
|
|
|
|
return AVObject.EnqueueForAll<object>(uniqueObjects, toAwait =>
|
|
{
|
|
var states = uniqueObjects.Select(t => t.state).ToList();
|
|
return toAwait.OnSuccess(_ =>
|
|
{
|
|
var deleteTasks = ObjectController.DeleteAllAsync(states,
|
|
AVUser.CurrentSessionToken,
|
|
cancellationToken);
|
|
|
|
return Task.WhenAll(deleteTasks);
|
|
}).Unwrap().OnSuccess(t =>
|
|
{
|
|
// Dirty all objects in memory.
|
|
foreach (var obj in uniqueObjects)
|
|
{
|
|
obj.IsDirty = true;
|
|
}
|
|
|
|
return (object)null;
|
|
});
|
|
}, cancellationToken);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private static void CollectDirtyChildren(object node,
|
|
IList<AVObject> dirtyChildren,
|
|
ICollection<AVObject> seen,
|
|
ICollection<AVObject> seenNew)
|
|
{
|
|
foreach (var obj in DeepTraversal(node).OfType<AVObject>())
|
|
{
|
|
ICollection<AVObject> scopedSeenNew;
|
|
// Check for cycles of new objects. Any such cycle means it will be impossible to save
|
|
// this collection of objects, so throw an exception.
|
|
if (obj.ObjectId != null)
|
|
{
|
|
scopedSeenNew = new HashSet<AVObject>(new IdentityEqualityComparer<AVObject>());
|
|
}
|
|
else
|
|
{
|
|
if (seenNew.Contains(obj))
|
|
{
|
|
throw new InvalidOperationException("Found a circular dependency while saving");
|
|
}
|
|
scopedSeenNew = new HashSet<AVObject>(seenNew, new IdentityEqualityComparer<AVObject>());
|
|
scopedSeenNew.Add(obj);
|
|
}
|
|
|
|
// Check for cycles of any object. If this occurs, then there's no problem, but
|
|
// we shouldn't recurse any deeper, because it would be an infinite recursion.
|
|
if (seen.Contains(obj))
|
|
{
|
|
return;
|
|
}
|
|
seen.Add(obj);
|
|
|
|
// Recurse into this object's children looking for dirty children.
|
|
// We only need to look at the child object's current estimated data,
|
|
// because that's the only data that might need to be saved now.
|
|
CollectDirtyChildren(obj.estimatedData, dirtyChildren, seen, scopedSeenNew);
|
|
|
|
if (obj.CheckIsDirty(false))
|
|
{
|
|
dirtyChildren.Add(obj);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper version of CollectDirtyChildren so that callers don't have to add the internally
|
|
/// used parameters.
|
|
/// </summary>
|
|
private static void CollectDirtyChildren(object node, IList<AVObject> dirtyChildren)
|
|
{
|
|
CollectDirtyChildren(node,
|
|
dirtyChildren,
|
|
new HashSet<AVObject>(new IdentityEqualityComparer<AVObject>()),
|
|
new HashSet<AVObject>(new IdentityEqualityComparer<AVObject>()));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the given object can be serialized for saving as a value
|
|
/// that is pointed to by a AVObject.
|
|
/// </summary>
|
|
private static bool CanBeSerializedAsValue(object value)
|
|
{
|
|
return DeepTraversal(value, yieldRoot: true)
|
|
.OfType<AVObject>()
|
|
.All(o => o.ObjectId != null);
|
|
}
|
|
|
|
private bool CanBeSerialized
|
|
{
|
|
get
|
|
{
|
|
// This method is only used for batching sets of objects for saveAll
|
|
// and when saving children automatically. Since it's only used to
|
|
// determine whether or not save should be called on them, it only
|
|
// needs to examine their current values, so we use estimatedData.
|
|
lock (mutex)
|
|
{
|
|
return CanBeSerializedAsValue(estimatedData);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a task to the queue for all of the given objects.
|
|
/// </summary>
|
|
private static Task<T> EnqueueForAll<T>(IEnumerable<AVObject> objects,
|
|
Func<Task, Task<T>> taskStart, CancellationToken cancellationToken)
|
|
{
|
|
// The task that will be complete when all of the child queues indicate they're ready to start.
|
|
var readyToStart = new TaskCompletionSource<object>();
|
|
|
|
// First, we need to lock the mutex for the queue for every object. We have to hold this
|
|
// from at least when taskStart() is called to when obj.taskQueue enqueue is called, so
|
|
// that saves actually get executed in the order they were setup by taskStart().
|
|
// The locks have to be sorted so that we always acquire them in the same order.
|
|
// Otherwise, there's some risk of deadlock.
|
|
var lockSet = new LockSet(objects.Select(o => o.taskQueue.Mutex));
|
|
|
|
lockSet.Enter();
|
|
try
|
|
{
|
|
|
|
// The task produced by taskStart. By running this immediately, we allow everything prior
|
|
// to toAwait to run before waiting for all of the queues on all of the objects.
|
|
Task<T> fullTask = taskStart(readyToStart.Task);
|
|
|
|
// Add fullTask to each of the objects' queues.
|
|
var childTasks = new List<Task>();
|
|
foreach (AVObject obj in objects)
|
|
{
|
|
obj.taskQueue.Enqueue((Task task) =>
|
|
{
|
|
childTasks.Add(task);
|
|
return fullTask;
|
|
}, cancellationToken);
|
|
}
|
|
|
|
// When all of the objects' queues are ready, signal fullTask that it's ready to go on.
|
|
Task.WhenAll(childTasks.ToArray()).ContinueWith((Task task) =>
|
|
{
|
|
readyToStart.SetResult(null);
|
|
});
|
|
|
|
return fullTask;
|
|
}
|
|
finally
|
|
{
|
|
lockSet.Exit();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a key from the object's data if it exists.
|
|
/// </summary>
|
|
/// <param name="key">The key to remove.</param>
|
|
public virtual void Remove(string key)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
CheckKeyIsMutable(key);
|
|
|
|
PerformOperation(key, AVDeleteOperation.Instance);
|
|
}
|
|
}
|
|
|
|
|
|
private IEnumerable<string> ApplyOperations(IDictionary<string,
|
|
IAVFieldOperation> operations,
|
|
IDictionary<string, object> map)
|
|
{
|
|
List<string> appliedKeys = new List<string>();
|
|
lock (mutex)
|
|
{
|
|
foreach (var pair in operations)
|
|
{
|
|
object oldValue;
|
|
map.TryGetValue(pair.Key, out oldValue);
|
|
var newValue = pair.Value.Apply(oldValue, pair.Key);
|
|
if (newValue != AVDeleteOperation.DeleteToken)
|
|
{
|
|
map[pair.Key] = newValue;
|
|
}
|
|
else
|
|
{
|
|
map.Remove(pair.Key);
|
|
}
|
|
appliedKeys.Add(pair.Key);
|
|
}
|
|
}
|
|
return appliedKeys;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Regenerates the estimatedData map from the serverData and operations.
|
|
/// </summary>
|
|
internal void RebuildEstimatedData()
|
|
{
|
|
IEnumerable<string> changedKeys = null;
|
|
|
|
lock (mutex)
|
|
{
|
|
//estimatedData.Clear();
|
|
List<string> converdKeys = new List<string>();
|
|
foreach (var item in state)
|
|
{
|
|
var key = item.Key;
|
|
var value = item.Value;
|
|
if (!estimatedData.ContainsKey(key))
|
|
{
|
|
converdKeys.Add(key);
|
|
}
|
|
else
|
|
{
|
|
var oldValue = estimatedData[key];
|
|
if (oldValue != value)
|
|
{
|
|
converdKeys.Add(key);
|
|
}
|
|
estimatedData.Remove(key);
|
|
}
|
|
estimatedData.Add(item);
|
|
}
|
|
changedKeys = converdKeys;
|
|
foreach (var operations in operationSetQueue)
|
|
{
|
|
var appliedKeys = ApplyOperations(operations, estimatedData);
|
|
changedKeys = converdKeys.Concat(appliedKeys);
|
|
}
|
|
// We've just applied a bunch of operations to estimatedData which
|
|
// may have changed all of its keys. Notify of all keys and properties
|
|
// mapped to keys being changed.
|
|
OnFieldsChanged(changedKeys);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// PerformOperation is like setting a value at an index, but instead of
|
|
/// just taking a new value, it takes a AVFieldOperation that modifies the value.
|
|
/// </summary>
|
|
internal void PerformOperation(string key, IAVFieldOperation operation)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
var ifDirtyBeforePerform = this.IsDirty;
|
|
object oldValue;
|
|
estimatedData.TryGetValue(key, out oldValue);
|
|
object newValue = operation.Apply(oldValue, key);
|
|
if (newValue != AVDeleteOperation.DeleteToken)
|
|
{
|
|
estimatedData[key] = newValue;
|
|
}
|
|
else
|
|
{
|
|
estimatedData.Remove(key);
|
|
}
|
|
|
|
IAVFieldOperation oldOperation;
|
|
bool wasDirty = CurrentOperations.Count > 0;
|
|
CurrentOperations.TryGetValue(key, out oldOperation);
|
|
var newOperation = operation.MergeWithPrevious(oldOperation);
|
|
CurrentOperations[key] = newOperation;
|
|
if (!wasDirty)
|
|
{
|
|
OnPropertyChanged("IsDirty");
|
|
if (ifDirtyBeforePerform != wasDirty)
|
|
{
|
|
OnPropertyUpdated("IsDirty", ifDirtyBeforePerform, wasDirty);
|
|
}
|
|
}
|
|
|
|
OnFieldsChanged(new[] { key });
|
|
OnPropertyUpdated(key, oldValue, newValue);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Override to run validations on key/value pairs. Make sure to still
|
|
/// call the base version.
|
|
/// </summary>
|
|
internal virtual void OnSettingValue(ref string key, ref object value)
|
|
{
|
|
if (key == null)
|
|
{
|
|
throw new ArgumentNullException("key");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value on the object. It is recommended to name
|
|
/// keys in partialCamelCaseLikeThis.
|
|
/// </summary>
|
|
/// <param name="key">The key for the object. Keys must be alphanumeric plus underscore
|
|
/// and start with a letter.</param>
|
|
/// <exception cref="System.Collections.Generic.KeyNotFoundException">The property is
|
|
/// retrieved and <paramref name="key"/> is not found.</exception>
|
|
/// <returns>The value for the key.</returns>
|
|
virtual public object this[string key]
|
|
{
|
|
get
|
|
{
|
|
lock (mutex)
|
|
{
|
|
CheckGetAccess(key);
|
|
|
|
var value = estimatedData[key];
|
|
|
|
// A relation may be deserialized without a parent or key. Either way,
|
|
// make sure it's consistent.
|
|
var relation = value as AVRelationBase;
|
|
if (relation != null)
|
|
{
|
|
relation.EnsureParentAndKey(this, key);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}
|
|
set
|
|
{
|
|
lock (mutex)
|
|
{
|
|
CheckKeyIsMutable(key);
|
|
|
|
Set(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform Set internally which is not gated by mutability check.
|
|
/// </summary>
|
|
/// <param name="key">key for the object.</param>
|
|
/// <param name="value">the value for the key.</param>
|
|
internal void Set(string key, object value)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
OnSettingValue(ref key, ref value);
|
|
|
|
if (!AVEncoder.IsValidType(value))
|
|
{
|
|
throw new ArgumentException("Invalid type for value: " + value.GetType().ToString());
|
|
}
|
|
|
|
PerformOperation(key, new AVSetOperation(value));
|
|
}
|
|
}
|
|
|
|
internal void SetIfDifferent<T>(string key, T value)
|
|
{
|
|
T current;
|
|
bool hasCurrent = TryGetValue<T>(key, out current);
|
|
if (value == null)
|
|
{
|
|
if (hasCurrent)
|
|
{
|
|
PerformOperation(key, AVDeleteOperation.Instance);
|
|
}
|
|
return;
|
|
}
|
|
if (!hasCurrent || !value.Equals(current))
|
|
{
|
|
Set(key, value);
|
|
}
|
|
}
|
|
|
|
#region Atomic Increment
|
|
|
|
/// <summary>
|
|
/// Atomically increments the given key by 1.
|
|
/// </summary>
|
|
/// <param name="key">The key to increment.</param>
|
|
public void Increment(string key)
|
|
{
|
|
Increment(key, 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Atomically increments the given key by the given number.
|
|
/// </summary>
|
|
/// <param name="key">The key to increment.</param>
|
|
/// <param name="amount">The amount to increment by.</param>
|
|
public void Increment(string key, long amount)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
CheckKeyIsMutable(key);
|
|
|
|
PerformOperation(key, new AVIncrementOperation(amount));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Atomically increments the given key by the given number.
|
|
/// </summary>
|
|
/// <param name="key">The key to increment.</param>
|
|
/// <param name="amount">The amount to increment by.</param>
|
|
public void Increment(string key, double amount)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
CheckKeyIsMutable(key);
|
|
|
|
PerformOperation(key, new AVIncrementOperation(amount));
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Atomically adds an object to the end of the list associated with the given key.
|
|
/// </summary>
|
|
/// <param name="key">The key.</param>
|
|
/// <param name="value">The object to add.</param>
|
|
public void AddToList(string key, object value)
|
|
{
|
|
AddRangeToList(key, new[] { value });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Atomically adds objects to the end of the list associated with the given key.
|
|
/// </summary>
|
|
/// <param name="key">The key.</param>
|
|
/// <param name="values">The objects to add.</param>
|
|
public void AddRangeToList<T>(string key, IEnumerable<T> values)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
CheckKeyIsMutable(key);
|
|
|
|
PerformOperation(key, new AVAddOperation(values.Cast<object>()));
|
|
|
|
OnCollectionPropertyUpdated(key, NotifyCollectionUpdatedAction.Add, null, values);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Atomically adds an object to the end of the list associated with the given key,
|
|
/// only if it is not already present in the list. The position of the insert is not
|
|
/// guaranteed.
|
|
/// </summary>
|
|
/// <param name="key">The key.</param>
|
|
/// <param name="value">The object to add.</param>
|
|
public void AddUniqueToList(string key, object value)
|
|
{
|
|
AddRangeUniqueToList(key, new object[] { value });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Atomically adds objects to the end of the list associated with the given key,
|
|
/// only if they are not already present in the list. The position of the inserts are not
|
|
/// guaranteed.
|
|
/// </summary>
|
|
/// <param name="key">The key.</param>
|
|
/// <param name="values">The objects to add.</param>
|
|
public void AddRangeUniqueToList<T>(string key, IEnumerable<T> values)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
CheckKeyIsMutable(key);
|
|
|
|
PerformOperation(key, new AVAddUniqueOperation(values.Cast<object>()));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Atomically removes all instances of the objects in <paramref name="values"/>
|
|
/// from the list associated with the given key.
|
|
/// </summary>
|
|
/// <param name="key">The key.</param>
|
|
/// <param name="values">The objects to remove.</param>
|
|
public void RemoveAllFromList<T>(string key, IEnumerable<T> values)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
CheckKeyIsMutable(key);
|
|
|
|
PerformOperation(key, new AVRemoveOperation(values.Cast<object>()));
|
|
|
|
OnCollectionPropertyUpdated(key, NotifyCollectionUpdatedAction.Remove, values, null);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether this object has a particular key.
|
|
/// </summary>
|
|
/// <param name="key">The key to check for</param>
|
|
public bool ContainsKey(string key)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return estimatedData.ContainsKey(key);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a value for the key of a particular type.
|
|
/// <typeparam name="T">The type to convert the value to. Supported types are
|
|
/// AVObject and its descendents, LeanCloud types such as AVRelation and AVGeopoint,
|
|
/// primitive types,IList<T>, IDictionary<string, T>, and strings.</typeparam>
|
|
/// <param name="key">The key of the element to get.</param>
|
|
/// <exception cref="System.Collections.Generic.KeyNotFoundException">The property is
|
|
/// retrieved and <paramref name="key"/> is not found.</exception>
|
|
/// </summary>
|
|
public T Get<T>(string key)
|
|
{
|
|
return Conversion.To<T>(this[key]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Access or create a Relation value for a key.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of object to create a relation for.</typeparam>
|
|
/// <param name="key">The key for the relation field.</param>
|
|
/// <returns>A AVRelation for the key.</returns>
|
|
public AVRelation<T> GetRelation<T>(string key) where T : AVObject
|
|
{
|
|
// All the sanity checking is done when add or remove is called.
|
|
AVRelation<T> relation = null;
|
|
TryGetValue(key, out relation);
|
|
return relation ?? new AVRelation<T>(this, key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get relation revserse query.
|
|
/// </summary>
|
|
/// <typeparam name="T">AVObject</typeparam>
|
|
/// <param name="parentClassName">parent className</param>
|
|
/// <param name="key">key</param>
|
|
/// <returns></returns>
|
|
public AVQuery<T> GetRelationRevserseQuery<T>(string parentClassName, string key) where T : AVObject
|
|
{
|
|
if (string.IsNullOrEmpty(parentClassName))
|
|
{
|
|
throw new ArgumentNullException("parentClassName", "can not query a relation without parentClassName.");
|
|
}
|
|
if (string.IsNullOrEmpty(key))
|
|
{
|
|
throw new ArgumentNullException("key", "can not query a relation without key.");
|
|
}
|
|
return new AVQuery<T>(parentClassName).WhereEqualTo(key, this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populates result with the value for the key, if possible.
|
|
/// </summary>
|
|
/// <typeparam name="T">The desired type for the value.</typeparam>
|
|
/// <param name="key">The key to retrieve a value for.</param>
|
|
/// <param name="result">The value for the given key, converted to the
|
|
/// requested type, or null if unsuccessful.</param>
|
|
/// <returns>true if the lookup and conversion succeeded, otherwise
|
|
/// false.</returns>
|
|
public virtual bool TryGetValue<T>(string key, out T result)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
if (ContainsKey(key))
|
|
{
|
|
try
|
|
{
|
|
var temp = Conversion.To<T>(this[key]);
|
|
result = temp;
|
|
return true;
|
|
}
|
|
catch (InvalidCastException)
|
|
{
|
|
result = default(T);
|
|
return false;
|
|
}
|
|
}
|
|
result = default(T);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets whether the AVObject has been fetched.
|
|
/// </summary>
|
|
public bool IsDataAvailable
|
|
{
|
|
get
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return hasBeenFetched;
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool CheckIsDataAvailable(string key)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return IsDataAvailable || estimatedData.ContainsKey(key);
|
|
}
|
|
}
|
|
|
|
private void CheckGetAccess(string key)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
if (!CheckIsDataAvailable(key))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"AVObject has no data for this key. Call FetchIfNeededAsync() to get the data.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CheckKeyIsMutable(string key)
|
|
{
|
|
if (!IsKeyMutable(key))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Cannot change the `" + key + "` property of a `" + ClassName + "` object.");
|
|
}
|
|
}
|
|
|
|
protected virtual bool IsKeyMutable(string key)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A helper function for checking whether two AVObjects point to
|
|
/// the same object in the cloud.
|
|
/// </summary>
|
|
public bool HasSameId(AVObject other)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return other != null &&
|
|
object.Equals(ClassName, other.ClassName) &&
|
|
object.Equals(ObjectId, other.ObjectId);
|
|
}
|
|
}
|
|
|
|
internal IDictionary<string, IAVFieldOperation> CurrentOperations
|
|
{
|
|
get
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return operationSetQueue.Last.Value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a set view of the keys contained in this object. This does not include createdAt,
|
|
/// updatedAt, or objectId. It does include things like username and ACL.
|
|
/// </summary>
|
|
public ICollection<string> Keys
|
|
{
|
|
get
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return estimatedData.Keys;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the AVACL governing this object.
|
|
/// </summary>
|
|
[AVFieldName("ACL")]
|
|
public AVACL ACL
|
|
{
|
|
get { return GetProperty<AVACL>(null, "ACL"); }
|
|
set { SetProperty(value, "ACL"); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if this object was created by the LeanCloud server when the
|
|
/// object might have already been there (e.g. in the case of a Facebook
|
|
/// login)
|
|
/// </summary>
|
|
#if !UNITY
|
|
public
|
|
#else
|
|
internal
|
|
#endif
|
|
bool IsNew
|
|
{
|
|
get
|
|
{
|
|
return state.IsNew;
|
|
}
|
|
#if !UNITY
|
|
internal
|
|
#endif
|
|
set
|
|
{
|
|
MutateState(mutableClone =>
|
|
{
|
|
mutableClone.IsNew = value;
|
|
});
|
|
OnPropertyChanged("IsNew");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the last time this object was updated as the server sees it, so that if you make changes
|
|
/// to a AVObject, then wait a while, and then call <see cref="SaveAsync()"/>, the updated time
|
|
/// will be the time of the <see cref="SaveAsync()"/> call rather than the time the object was
|
|
/// changed locally.
|
|
/// </summary>
|
|
[AVFieldName("updatedAt")]
|
|
public DateTime? UpdatedAt
|
|
{
|
|
get
|
|
{
|
|
return state.UpdatedAt;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the first time this object was saved as the server sees it, so that if you create a
|
|
/// AVObject, then wait a while, and then call <see cref="SaveAsync()"/>, the
|
|
/// creation time will be the time of the first <see cref="SaveAsync()"/> call rather than
|
|
/// the time the object was created locally.
|
|
/// </summary>
|
|
[AVFieldName("createdAt")]
|
|
public DateTime? CreatedAt
|
|
{
|
|
get
|
|
{
|
|
return state.CreatedAt;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates whether this AVObject has unsaved changes.
|
|
/// </summary>
|
|
public bool IsDirty
|
|
{
|
|
get
|
|
{
|
|
lock (mutex) { return CheckIsDirty(true); }
|
|
}
|
|
internal set
|
|
{
|
|
lock (mutex)
|
|
{
|
|
dirty = value;
|
|
OnPropertyChanged("IsDirty");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indicates whether key is unsaved for this AVObject.
|
|
/// </summary>
|
|
/// <param name="key">The key to check for.</param>
|
|
/// <returns><c>true</c> if the key has been altered and not saved yet, otherwise
|
|
/// <c>false</c>.</returns>
|
|
public bool IsKeyDirty(string key)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return CurrentOperations.ContainsKey(key);
|
|
}
|
|
}
|
|
|
|
private bool CheckIsDirty(bool considerChildren)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return (dirty || CurrentOperations.Count > 0 || (considerChildren && HasDirtyChildren));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the object id. An object id is assigned as soon as an object is
|
|
/// saved to the server. The combination of a <see cref="ClassName"/> and an
|
|
/// <see cref="ObjectId"/> uniquely identifies an object in your application.
|
|
/// </summary>
|
|
[AVFieldName("objectId")]
|
|
public string ObjectId
|
|
{
|
|
get
|
|
{
|
|
return state.ObjectId;
|
|
}
|
|
set
|
|
{
|
|
IsDirty = true;
|
|
SetObjectIdInternal(value);
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// Sets the objectId without marking dirty.
|
|
/// </summary>
|
|
/// <param name="objectId">The new objectId</param>
|
|
private void SetObjectIdInternal(string objectId)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
MutateState(mutableClone =>
|
|
{
|
|
mutableClone.ObjectId = objectId;
|
|
});
|
|
OnPropertyChanged("ObjectId");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the class name for the AVObject.
|
|
/// </summary>
|
|
public string ClassName
|
|
{
|
|
get
|
|
{
|
|
return state.ClassName;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a value for the given key, throwing an Exception if the key
|
|
/// already has a value.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This allows you to use collection initialization syntax when creating AVObjects,
|
|
/// such as:
|
|
/// <code>
|
|
/// var obj = new AVObject("MyType")
|
|
/// {
|
|
/// {"name", "foo"},
|
|
/// {"count", 10},
|
|
/// {"found", false}
|
|
/// };
|
|
/// </code>
|
|
/// </remarks>
|
|
/// <param name="key">The key for which a value should be set.</param>
|
|
/// <param name="value">The value for the key.</param>
|
|
public void Add(string key, object value)
|
|
{
|
|
lock (mutex)
|
|
{
|
|
if (this.ContainsKey(key))
|
|
{
|
|
throw new ArgumentException("Key already exists", key);
|
|
}
|
|
this[key] = value;
|
|
}
|
|
}
|
|
|
|
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>
|
|
.GetEnumerator()
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return estimatedData.GetEnumerator();
|
|
}
|
|
}
|
|
|
|
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
|
|
{
|
|
lock (mutex)
|
|
{
|
|
return ((IEnumerable<KeyValuePair<string, object>>)this).GetEnumerator();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a <see cref="AVQuery{AVObject}"/> for the type of object specified by
|
|
/// <paramref name="className"/>
|
|
/// </summary>
|
|
/// <param name="className">The class name of the object.</param>
|
|
/// <returns>A new <see cref="AVQuery{AVObject}"/>.</returns>
|
|
public static AVQuery<AVObject> GetQuery(string className)
|
|
{
|
|
// Since we can't return a AVQuery<AVUser> (due to strong-typing with
|
|
// generics), we'll require you to go through subclasses. This is a better
|
|
// experience anyway, especially with LINQ integration, since you'll get
|
|
// strongly-typed queries and compile-time checking of property names and
|
|
// types.
|
|
if (SubclassingController.GetType(className) != null)
|
|
{
|
|
throw new ArgumentException(
|
|
"Use the class-specific query properties for class " + className, "className");
|
|
}
|
|
return new AVQuery<AVObject>(className);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises change notifications for all properties associated with the given
|
|
/// field names. If fieldNames is null, this will notify for all known field-linked
|
|
/// properties (e.g. this happens when we recalculate all estimated data from scratch)
|
|
/// </summary>
|
|
protected virtual void OnFieldsChanged(IEnumerable<string> fieldNames)
|
|
{
|
|
var mappings = SubclassingController.GetPropertyMappings(ClassName);
|
|
IEnumerable<String> properties;
|
|
|
|
if (fieldNames != null && mappings != null)
|
|
{
|
|
properties = from m in mappings
|
|
join f in fieldNames on m.Value equals f
|
|
select m.Key;
|
|
}
|
|
else if (mappings != null)
|
|
{
|
|
properties = mappings.Keys;
|
|
}
|
|
else
|
|
{
|
|
properties = Enumerable.Empty<String>();
|
|
}
|
|
|
|
foreach (var property in properties)
|
|
{
|
|
OnPropertyChanged(property);
|
|
}
|
|
OnPropertyChanged("Item[]");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises change notifications for a property. Passing null or the empty string
|
|
/// notifies the binding framework that all properties/indexes have changed.
|
|
/// Passing "Item[]" tells the binding framework that all indexed values
|
|
/// have changed (but not all properties)
|
|
/// </summary>
|
|
protected virtual void OnPropertyChanged(
|
|
#if !UNITY
|
|
[CallerMemberName] string propertyName = null
|
|
#else
|
|
string propertyName
|
|
#endif
|
|
)
|
|
{
|
|
propertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
|
|
private SynchronizedEventHandler<PropertyChangedEventArgs> propertyChanged =
|
|
new SynchronizedEventHandler<PropertyChangedEventArgs>();
|
|
/// <summary>
|
|
/// Occurs when a property value changes.
|
|
/// </summary>
|
|
public event PropertyChangedEventHandler PropertyChanged
|
|
{
|
|
add
|
|
{
|
|
propertyChanged.Add(value);
|
|
}
|
|
remove
|
|
{
|
|
propertyChanged.Remove(value);
|
|
}
|
|
}
|
|
|
|
private SynchronizedEventHandler<PropertyUpdatedEventArgs> propertyUpdated =
|
|
new SynchronizedEventHandler<PropertyUpdatedEventArgs>();
|
|
|
|
public event PropertyUpdatedEventHandler PropertyUpdated
|
|
{
|
|
add
|
|
{
|
|
propertyUpdated.Add(value);
|
|
}
|
|
remove
|
|
{
|
|
propertyUpdated.Remove(value);
|
|
}
|
|
}
|
|
|
|
protected virtual void OnPropertyUpdated(string propertyName, object newValue, object oldValue)
|
|
{
|
|
propertyUpdated.Invoke(this, new PropertyUpdatedEventArgs(propertyName, oldValue, newValue));
|
|
}
|
|
|
|
private SynchronizedEventHandler<CollectionPropertyUpdatedEventArgs> collectionUpdated =
|
|
new SynchronizedEventHandler<CollectionPropertyUpdatedEventArgs>();
|
|
|
|
public event CollectionPropertyUpdatedEventHandler CollectionPropertyUpdated
|
|
{
|
|
add
|
|
{
|
|
collectionUpdated.Add(value);
|
|
}
|
|
remove
|
|
{
|
|
collectionUpdated.Remove(value);
|
|
}
|
|
}
|
|
|
|
protected virtual void OnCollectionPropertyUpdated(string propertyName, NotifyCollectionUpdatedAction action, IEnumerable oldValues, IEnumerable newValues)
|
|
{
|
|
collectionUpdated?.Invoke(this, new CollectionPropertyUpdatedEventArgs(propertyName, action, oldValues, newValues));
|
|
}
|
|
}
|
|
|
|
public interface INotifyPropertyUpdated
|
|
{
|
|
event PropertyUpdatedEventHandler PropertyUpdated;
|
|
}
|
|
|
|
public interface INotifyCollectionPropertyUpdated
|
|
{
|
|
event CollectionPropertyUpdatedEventHandler CollectionPropertyUpdated;
|
|
}
|
|
|
|
public enum NotifyCollectionUpdatedAction
|
|
{
|
|
Add,
|
|
Remove
|
|
}
|
|
|
|
public class CollectionPropertyUpdatedEventArgs : PropertyChangedEventArgs
|
|
{
|
|
public CollectionPropertyUpdatedEventArgs(string propertyName, NotifyCollectionUpdatedAction collectionAction, IEnumerable oldValues, IEnumerable newValues) : base(propertyName)
|
|
{
|
|
CollectionAction = collectionAction;
|
|
OldValues = oldValues;
|
|
NewValues = newValues;
|
|
}
|
|
|
|
public IEnumerable OldValues { get; set; }
|
|
|
|
public IEnumerable NewValues { get; set; }
|
|
|
|
public NotifyCollectionUpdatedAction CollectionAction { get; set; }
|
|
}
|
|
|
|
public class PropertyUpdatedEventArgs : PropertyChangedEventArgs
|
|
{
|
|
public PropertyUpdatedEventArgs(string propertyName, object oldValue, object newValue) : base(propertyName)
|
|
{
|
|
OldValue = oldValue;
|
|
NewValue = newValue;
|
|
}
|
|
|
|
public object OldValue { get; private set; }
|
|
public object NewValue { get; private set; }
|
|
}
|
|
|
|
public delegate void PropertyUpdatedEventHandler(object sender, PropertyUpdatedEventArgs args);
|
|
|
|
public delegate void CollectionPropertyUpdatedEventHandler(object sender, CollectionPropertyUpdatedEventArgs args);
|
|
}
|