Last active
May 15, 2021 10:58
-
-
Save fairking/7c07a5af6af49d5da8ccd285020a66ec to your computer and use it in GitHub Desktop.
ObservableInterceptor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Reflection; | |
using Castle.DynamicProxy; | |
namespace MyTest.Observable | |
{ | |
internal class ObservableInterceptor : IInterceptor | |
{ | |
public bool IsChanged { get; protected set; } | |
private HashSet<string> _changedProperties = new HashSet<string>(); | |
public IReadOnlySet<string> ChangedProperties => _changedProperties; | |
public void Intercept(IInvocation invocation) | |
{ | |
if (IsSetter(invocation.Method)) | |
{ | |
var propertyName = invocation.Method.Name.Substring(4); | |
var property = invocation.TargetType.GetProperty(propertyName); | |
var oldValue = property.GetValue(invocation.InvocationTarget); | |
var newValue = invocation.Arguments[0]; | |
if (!object.Equals(oldValue, newValue)) | |
{ | |
IsChanged = true; | |
_changedProperties.Add(propertyName); | |
} | |
} | |
invocation.Proceed(); | |
} | |
public void ClearChanges() | |
{ | |
IsChanged = false; | |
_changedProperties.Clear(); | |
} | |
private bool IsSetter(MethodInfo method) | |
{ | |
return method.IsSpecialName && method.Name.StartsWith("set_", StringComparison.OrdinalIgnoreCase); | |
} | |
} | |
public class ObservableProxy | |
{ | |
private static readonly ProxyGenerator Generator = new ProxyGenerator(); | |
public static T Create<T>() where T : class, new() | |
{ | |
return Generator.CreateClassProxy<T>(new ObservableInterceptor()); | |
} | |
public static T Create<T>(T obj) where T : class | |
{ | |
return Generator.CreateClassProxyWithTarget(obj, new ObservableInterceptor()); | |
} | |
public static bool IsProxy<T>(T obj) where T : class | |
{ | |
return ProxyUtil.IsProxy(obj); | |
} | |
public static T Unproxy<T>(T obj) where T : class | |
{ | |
if (!ProxyUtil.IsProxy(obj)) | |
return obj; | |
const System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; | |
var proxyType = obj.GetType(); | |
var targetField = proxyType.GetField("__target", flags); | |
var target = targetField.GetValue(obj); | |
return (T)target; | |
} | |
public static bool IsChanged<T>(T obj) where T : class | |
{ | |
if (!IsProxy(obj)) | |
throw new ArgumentException("Cannot check changes because the object is not a proxy."); | |
var interceptor = GetInterceptor(obj) | |
?? throw new ArgumentException("Cannot check changes because the proxy has no ObservableInterceptor implemented."); | |
return interceptor.IsChanged; | |
} | |
public static string[] ChangedProperties<T>(T obj) where T : class | |
{ | |
if (!IsProxy(obj)) | |
throw new ArgumentException("Cannot check changes because the object is not a proxy."); | |
var interceptor = GetInterceptor(obj) | |
?? throw new ArgumentException("Cannot check changes because the proxy has no ObservableInterceptor implemented."); | |
return interceptor.ChangedProperties.ToArray(); | |
} | |
public static void ClearChanged<T>(T obj) where T : class | |
{ | |
if (!IsProxy(obj)) | |
throw new ArgumentException("Cannot check changes because the object is not a proxy."); | |
var interceptor = GetInterceptor(obj) | |
?? throw new ArgumentException("Cannot check changes because the proxy has no ObservableInterceptor implemented."); | |
interceptor.ClearChanges(); | |
} | |
private static ObservableInterceptor GetInterceptor<T>(T obj) where T : class | |
{ | |
const System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance; | |
var proxyType = obj.GetType(); | |
var interceptorsField = proxyType.GetField("__interceptors", flags); | |
var interceptor = (interceptorsField.GetValue(obj) as IEnumerable).OfType<ObservableInterceptor>().SingleOrDefault(); | |
if (interceptor == null) | |
throw new ArgumentException("Cannot check changes because the proxy has no ObservableInterceptor implemented."); | |
return interceptor; | |
} | |
} | |
public class MyEntity | |
{ | |
public virtual int Id { get; protected set; } | |
public virtual string Name { get; set; } | |
public virtual string Description { get; set; } | |
} | |
public class ObservableInterceptorTests | |
{ | |
[Fact] | |
public void interceptor_is_working() | |
{ | |
var obj = new MyEntity() | |
{ | |
Name = "My name", | |
Description = "My description" | |
}; | |
var proxy = ObservableProxy.Create(obj); | |
Assert.Equal("My name", proxy.Name); | |
Assert.False(ObservableProxy.IsChanged(proxy)); | |
proxy.Name = "My name"; | |
Assert.Equal("My name", proxy.Name); | |
Assert.False(ObservableProxy.IsChanged(proxy)); | |
proxy.Name = "My name 2"; | |
Assert.Equal("My name 2", proxy.Name); | |
Assert.True(ObservableProxy.IsChanged(proxy)); | |
Assert.Collection(ObservableProxy.ChangedProperties(proxy), item => Assert.Equal("Name", item)); | |
ObservableProxy.ClearChanged(proxy); | |
Assert.False(ObservableProxy.IsChanged(proxy)); | |
Assert.Empty(ObservableProxy.ChangedProperties(proxy)); | |
var obj2 = ObservableProxy.Unproxy(proxy); | |
Assert.Throws<ArgumentException>(() => ObservableProxy.IsChanged(obj2)); | |
Assert.Throws<ArgumentException>(() => ObservableProxy.ChangedProperties(obj2)); | |
Assert.Throws<ArgumentException>(() => ObservableProxy.ClearChanged(obj2)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment