diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 28a2c0af63..0e2037c65e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -35,6 +35,8 @@ src/ReactiveUI.Blend/* @reactiveui/wpf-team @reactiveui/winf src/ReactiveUI.Events.WPF/* @reactiveui/wpf-team src/ReactiveUI.Events.XamForms/* @reactiveui/xamarin-forms-team +src/ReactiveUI.Fody*/* @reactiveui/fody-team + src/ReactiveUI.Winforms/* @reactiveui/winforms-team src/ReactiveUI.Wpf/* @reactiveui/wpf-team diff --git a/.gitignore b/.gitignore index 4c408bd5f0..c293217e4a 100644 --- a/.gitignore +++ b/.gitignore @@ -245,3 +245,6 @@ src/ReactiveUI.Events*/Events_*.cs src/*.Tests/API/*.received.txt .idea/ + +# Fody Weavers (for tests) +src/Tools/ \ No newline at end of file diff --git a/build.cake b/build.cake index 205494430b..4b5130b139 100644 --- a/build.cake +++ b/build.cake @@ -57,6 +57,8 @@ var packageWhitelist = new[] { "ReactiveUI.Testing", "ReactiveUI.Events.Winforms", "ReactiveUI.Events.XamForms", "ReactiveUI", + "ReactiveUI.Fody", + "ReactiveUI.Fody.Helpers", "ReactiveUI.AndroidSupport", "ReactiveUI.Blend", "ReactiveUI.WPF", @@ -198,6 +200,7 @@ Task("BuildReactiveUI") build("./src/ReactiveUI.Tests/ReactiveUI.Tests.csproj", "ReactiveUI.Tests"); build("./src/ReactiveUI.LeakTests/ReactiveUI.LeakTests.csproj", "ReactiveUI.LeakTests"); + build("./src/ReactiveUI.Fody.Tests/ReactiveUI.Fody.Tests.csproj", "ReactiveUI.Fody.Tests"); }); Task("RunUnitTests") @@ -205,8 +208,15 @@ Task("RunUnitTests") .Does(() => { Action testAction = tool => { + tool.XUnit2("./src/ReactiveUI.Tests/bin/**/*.Tests.dll", new XUnit2Settings { + OutputDirectory = artifactDirectory, + XmlReport = true, + NoAppDomain = true + }); + }; - tool.XUnit2("./src/ReactiveUI.*Tests/bin/**/*.Tests.dll", new XUnit2Settings { + Action testFodyAction = tool => { + tool.XUnit2("./src/ReactiveUI.Fody.Tests/bin/**/*.Tests.dll", new XUnit2Settings { OutputDirectory = artifactDirectory, XmlReport = true, NoAppDomain = true @@ -232,6 +242,25 @@ Task("RunUnitTests") .ExcludeByFile("*splat/splat*") .ExcludeByFile("*ApprovalTests*")); + OpenCover(testFodyAction, + testCoverageOutputFile, + new OpenCoverSettings { + ReturnTargetCodeOffset = 0, + ArgumentCustomization = args => args.Append("-mergeoutput") + } + .WithFilter("+[*]*") + .WithFilter("-[*.Testing]*") + .WithFilter("-[*.Tests*]*") + .WithFilter("-[ReactiveUI.Events]*") + .WithFilter("-[Splat*]*") + .WithFilter("-[ApprovalTests*]*") + .ExcludeByAttribute("*.ExcludeFromCodeCoverage*") + .ExcludeByFile("*/*Designer.cs") + .ExcludeByFile("*/*.g.cs") + .ExcludeByFile("*/*.g.i.cs") + .ExcludeByFile("*splat/splat*") + .ExcludeByFile("*ApprovalTests*")); + ReportGenerator(testCoverageOutputFile, artifactDirectory); }).ReportError(exception => { diff --git a/src/Directory.build.props b/src/Directory.build.props index a2790b24af..d5afbd8b31 100644 --- a/src/Directory.build.props +++ b/src/Directory.build.props @@ -33,7 +33,14 @@ - + + + + + + + + diff --git a/src/ReactiveUI.Fody.Helpers/ObservableAsPropertyAttribute.cs b/src/ReactiveUI.Fody.Helpers/ObservableAsPropertyAttribute.cs new file mode 100644 index 0000000000..3c1b2c275d --- /dev/null +++ b/src/ReactiveUI.Fody.Helpers/ObservableAsPropertyAttribute.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace ReactiveUI.Fody.Helpers +{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public class ObservableAsPropertyAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/ReactiveUI.Fody.Helpers/ObservableAsPropertyExtensions.cs b/src/ReactiveUI.Fody.Helpers/ObservableAsPropertyExtensions.cs new file mode 100644 index 0000000000..31ad9f836f --- /dev/null +++ b/src/ReactiveUI.Fody.Helpers/ObservableAsPropertyExtensions.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq.Expressions; +using System.Reactive.Concurrency; +using System.Reflection; + +namespace ReactiveUI.Fody.Helpers +{ + public static class ObservableAsPropertyExtensions + { + public static ObservableAsPropertyHelper ToPropertyEx(this IObservable @this, TObj source, Expression> property, TRet initialValue = default(TRet), bool deferSubscription = false, IScheduler scheduler = null) where TObj : ReactiveObject + { + var result = @this.ToProperty(source, property, initialValue, deferSubscription, scheduler); + + // Now assign the field via reflection. + var propertyInfo = property.GetPropertyInfo(); + if (propertyInfo == null) + throw new Exception("Could not resolve expression " + property + " into a property."); + + var field = propertyInfo.DeclaringType.GetTypeInfo().GetDeclaredField("$" + propertyInfo.Name); + if (field == null) + throw new Exception("Backing field not found for " + propertyInfo); + field.SetValue(source, result); + + return result; + } + + static PropertyInfo GetPropertyInfo(this LambdaExpression expression) + { + var current = expression.Body; + var unary = current as UnaryExpression; + if (unary != null) + current = unary.Operand; + var call = (MemberExpression)current; + return (PropertyInfo)call.Member; + } + } +} diff --git a/src/ReactiveUI.Fody.Helpers/Properties/AssemblyInfo.cs b/src/ReactiveUI.Fody.Helpers/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..4fa29309e5 --- /dev/null +++ b/src/ReactiveUI.Fody.Helpers/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ReactiveUI.Tests")] +[assembly: InternalsVisibleTo("ReactiveUI.Winforms")] +[assembly: InternalsVisibleTo("ReactiveUI.Wpf")] +[assembly: InternalsVisibleTo("ReactiveUI.XamForms")] +[assembly: InternalsVisibleTo("ReactiveUI.AndroidSupport")] diff --git a/src/ReactiveUI.Fody.Helpers/ReactiveAttribute.cs b/src/ReactiveUI.Fody.Helpers/ReactiveAttribute.cs new file mode 100644 index 0000000000..caa305568e --- /dev/null +++ b/src/ReactiveUI.Fody.Helpers/ReactiveAttribute.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace ReactiveUI.Fody.Helpers +{ + [AttributeUsage(AttributeTargets.Property)] + public class ReactiveAttribute : Attribute + { + } +} diff --git a/src/ReactiveUI.Fody.Helpers/ReactiveDependencyAttribute.cs b/src/ReactiveUI.Fody.Helpers/ReactiveDependencyAttribute.cs new file mode 100644 index 0000000000..065414b119 --- /dev/null +++ b/src/ReactiveUI.Fody.Helpers/ReactiveDependencyAttribute.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace ReactiveUI.Fody.Helpers +{ + [AttributeUsage(AttributeTargets.Property)] + public class ReactiveDependencyAttribute : Attribute + { + private readonly string _targetName; + + public ReactiveDependencyAttribute(string targetName) + { + _targetName = targetName; + } + + /// + /// The name of the backing property + /// + public string Target => _targetName; + + /// + /// Target property on the backing property + /// + public string TargetProperty { get; set; } + } +} diff --git a/src/ReactiveUI.Fody.Helpers/ReactiveUI.Fody.Helpers.csproj b/src/ReactiveUI.Fody.Helpers/ReactiveUI.Fody.Helpers.csproj new file mode 100644 index 0000000000..4713f23664 --- /dev/null +++ b/src/ReactiveUI.Fody.Helpers/ReactiveUI.Fody.Helpers.csproj @@ -0,0 +1,51 @@ + + + netstandard2.0;net461;uap10.0.16299;Xamarin.iOS10;Xamarin.Mac20;MonoAndroid80;netcoreapp2.0 + ReactiveUI.Fody.Helpers + ReactiveUI.Fody.Helpers + Fody extension to generate RaisePropertyChange notifications for properties and ObservableAsPropertyHelper properties. + + + ReactiveUI.Fody + ..\$(PackageId)\bin\$(Configuration)\ + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ReactiveUI.Fody.Helpers/ReactiveUI.Fody.Helpers.licenseheader b/src/ReactiveUI.Fody.Helpers/ReactiveUI.Fody.Helpers.licenseheader new file mode 100644 index 0000000000..712fdc59d7 --- /dev/null +++ b/src/ReactiveUI.Fody.Helpers/ReactiveUI.Fody.Helpers.licenseheader @@ -0,0 +1,12 @@ +extensions: designer.cs generated.cs +extensions: .cs +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +extensions: .xml .config .xsd + diff --git a/src/ReactiveUI.Fody.Tests/API/ApiApprovalTests.ReactiveUI_Fody.approved.txt b/src/ReactiveUI.Fody.Tests/API/ApiApprovalTests.ReactiveUI_Fody.approved.txt new file mode 100644 index 0000000000..8e2b29a884 --- /dev/null +++ b/src/ReactiveUI.Fody.Tests/API/ApiApprovalTests.ReactiveUI_Fody.approved.txt @@ -0,0 +1,31 @@ +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("ReactiveUI.AndroidSupport")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("ReactiveUI.Tests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("ReactiveUI.Winforms")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("ReactiveUI.Wpf")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("ReactiveUI.XamForms")] +[assembly: System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.6.1", FrameworkDisplayName=".NET Framework 4.6.1")] +namespace ReactiveUI.Fody.Helpers +{ + [System.AttributeUsageAttribute(System.AttributeTargets.Method | System.AttributeTargets.Property | System.AttributeTargets.All)] + public class ObservableAsPropertyAttribute : System.Attribute + { + public ObservableAsPropertyAttribute() { } + } + public class static ObservableAsPropertyExtensions + { + public static ReactiveUI.ObservableAsPropertyHelper ToPropertyEx(this System.IObservable @this, TObj source, System.Linq.Expressions.Expression> property, TRet initialValue = null, bool deferSubscription = False, System.Reactive.Concurrency.IScheduler scheduler = null) + where TObj : ReactiveUI.ReactiveObject { } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Property | System.AttributeTargets.All)] + public class ReactiveAttribute : System.Attribute + { + public ReactiveAttribute() { } + } + [System.AttributeUsageAttribute(System.AttributeTargets.Property | System.AttributeTargets.All)] + public class ReactiveDependencyAttribute : System.Attribute + { + public ReactiveDependencyAttribute(string targetName) { } + public string Target { get; } + public string TargetProperty { get; set; } + } +} \ No newline at end of file diff --git a/src/ReactiveUI.Fody.Tests/API/ApiApprovalTests.cs b/src/ReactiveUI.Fody.Tests/API/ApiApprovalTests.cs new file mode 100644 index 0000000000..b1ac60d9c3 --- /dev/null +++ b/src/ReactiveUI.Fody.Tests/API/ApiApprovalTests.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using ApprovalTests; +using ApprovalTests.Reporters; +using PublicApiGenerator; +using ReactiveUI.Fody.Helpers; +using Xunit; + +namespace ReactiveUI.Fody.Tests.API +{ + [ExcludeFromCodeCoverage] + [UseReporter(typeof(DiffReporter))] + public class ApiApprovalTests + { + + [Fact] + public void ReactiveUI_Fody() + { + var publicApi = Filter(ApiGenerator.GeneratePublicApi(typeof(ReactiveAttribute).Assembly)); + Approvals.Verify(publicApi); + } + + private static string Filter(string text) + { + return string.Join(Environment.NewLine, text.Split(new[] + { + Environment.NewLine + }, StringSplitOptions.RemoveEmptyEntries) + .Where(l => !l.StartsWith("[assembly: AssemblyVersion(")) + .Where(l => !l.StartsWith("[assembly: AssemblyFileVersion(")) + .Where(l => !l.StartsWith("[assembly: AssemblyInformationalVersion(")) + .Where(l => !string.IsNullOrWhiteSpace(l)) + ); + } + } +} diff --git a/src/ReactiveUI.Fody.Tests/FodyWeavers.xml b/src/ReactiveUI.Fody.Tests/FodyWeavers.xml new file mode 100644 index 0000000000..287688a724 --- /dev/null +++ b/src/ReactiveUI.Fody.Tests/FodyWeavers.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/src/ReactiveUI.Fody.Tests/Issues/Issue10Tests.cs b/src/ReactiveUI.Fody.Tests/Issues/Issue10Tests.cs new file mode 100644 index 0000000000..c56305d5a1 --- /dev/null +++ b/src/ReactiveUI.Fody.Tests/Issues/Issue10Tests.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using ReactiveUI.Fody.Helpers; +using Xunit; + +namespace ReactiveUI.Fody.Tests.Issues +{ + public class Issue10Tests + { + [Fact] + public void UninitializedObservableAsPropertyHelperDoesntThrowAndReturnsDefaultValue() + { + var model = new TestModel(); + Assert.Equal(null, model.MyProperty); + Assert.Equal(0, model.MyIntProperty); + Assert.Equal(default(DateTime), model.MyDateTimeProperty); + } + + class TestModel : ReactiveObject + { + [ObservableAsProperty] + public string MyProperty { get; private set; } + + [ObservableAsProperty] + public int MyIntProperty { get; private set; } + + [ObservableAsProperty] + public DateTime MyDateTimeProperty { get; private set; } + + public string OtherProperty { get; private set; } + + public TestModel() + { + OtherProperty = MyProperty; + } + } + } +} diff --git a/src/ReactiveUI.Fody.Tests/Issues/Issue11Tests.cs b/src/ReactiveUI.Fody.Tests/Issues/Issue11Tests.cs new file mode 100644 index 0000000000..00465e276f --- /dev/null +++ b/src/ReactiveUI.Fody.Tests/Issues/Issue11Tests.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System.Reactive.Linq; +using ReactiveUI.Fody.Helpers; +using Xunit; + +namespace ReactiveUI.Fody.Tests.Issues +{ + public class Issue11Tests + { + [Fact] + public void AllowObservableAsPropertyAttributeOnAccessor() + { + var model = new TestModel("foo"); + Assert.Equal("foo", model.MyProperty); + } + + public class TestModel : ReactiveObject + { + public extern string MyProperty { [ObservableAsProperty]get; } + + public TestModel(string myProperty) + { + Observable.Return(myProperty).ToPropertyEx(this, x => x.MyProperty); + } + } + } +} diff --git a/src/ReactiveUI.Fody.Tests/Issues/Issue13Tests.cs b/src/ReactiveUI.Fody.Tests/Issues/Issue13Tests.cs new file mode 100644 index 0000000000..78d2963668 --- /dev/null +++ b/src/ReactiveUI.Fody.Tests/Issues/Issue13Tests.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ReactiveUI.Fody.Tests.Issues +{ + public class Issue13Tests + { + [Fact] + public void AccessingAChainedObservableAsPropertyOfDoubleDoesntThrow() + { + var vm = new VM(); + Assert.Equal(0.0, vm.P2); + } + + class VM : ReactiveObject + { + [ObservableAsProperty] public double P1 { get; } + [ObservableAsProperty] public double P2 { get; } + + public VM() + { + Observable.Return(0.0).ToPropertyEx(this, vm => vm.P1); + this.WhenAnyValue(vm => vm.P1).ToPropertyEx(this, vm => vm.P2); + } + } + + } + +} diff --git a/src/ReactiveUI.Fody.Tests/ObservableAsPropertyTests.cs b/src/ReactiveUI.Fody.Tests/ObservableAsPropertyTests.cs new file mode 100644 index 0000000000..f4fccd7018 --- /dev/null +++ b/src/ReactiveUI.Fody.Tests/ObservableAsPropertyTests.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System.Reactive.Linq; +using ReactiveUI.Fody.Helpers; +using Xunit; + +namespace ReactiveUI.Fody.Tests +{ + public class ObservableAsPropertyTests + { + [Fact] + public void TestPropertyReturnsFoo() + { + var model = new TestModel(); + Assert.Equal("foo", model.TestProperty); + } + + public class TestModel : ReactiveObject + { + [ObservableAsProperty] + public string TestProperty { get; private set; } + + public TestModel() + { + Observable.Return("foo").ToPropertyEx(this, x => x.TestProperty); + } + } + } +} diff --git a/src/ReactiveUI.Fody.Tests/ReactiveDependencyTests.cs b/src/ReactiveUI.Fody.Tests/ReactiveDependencyTests.cs new file mode 100644 index 0000000000..82862edafa --- /dev/null +++ b/src/ReactiveUI.Fody.Tests/ReactiveDependencyTests.cs @@ -0,0 +1,206 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; +using ReactiveUI.Fody.Helpers; +using Xunit; + +namespace ReactiveUI.Fody.Tests +{ + public class ReactiveDependencyTests + { + [Fact] + public void IntPropertyOnWeavedFacadeReturnsBaseModelIntPropertyDefaultValueTest() + { + var model = new BaseModel(); + var expectedResult = model.IntProperty; + + var facade = new FacadeModel(model); + + Assert.Equal(expectedResult, facade.IntProperty); + } + + [Fact] + public void AnotherStringPropertyOnFacadeReturnsBaseModelStringPropertyDefaultValueTest() + { + var model = new BaseModel(); + var expectedResult = model.StringProperty; + + var facade = new FacadeModel(model); + + Assert.Equal(expectedResult, facade.AnotherStringProperty); + } + + [Fact] + public void SettingAnotherStringPropertyUpdatesTheDependencyStringProperty() + { + var expectedResult = "New String Value"; + var facade = new FacadeModel(new BaseModel()); + + facade.AnotherStringProperty = expectedResult; + + Assert.Equal(expectedResult, facade.Dependency.StringProperty); + } + + [Fact] + public void SettingFacadeIntPropertyUpdatesDependencyIntProperty() + { + var expectedResult = 999; + var facade = new FacadeModel(new BaseModel()); + + facade.IntProperty = expectedResult; + + Assert.Equal(expectedResult, facade.Dependency.IntProperty); + } + + [Fact] + public void FacadeIntPropertyChangedEventFiresOnAssignementTest() + { + var expectedPropertyChanged = "IntProperty"; + var resultPropertyChanged = string.Empty; + + var facade = new FacadeModel(new BaseModel()); + + var obj = (INotifyPropertyChanged) facade; + obj.PropertyChanged += (sender, args) => resultPropertyChanged = args.PropertyName; + + facade.IntProperty = 999; + + Assert.Equal(expectedPropertyChanged, resultPropertyChanged); + } + + [Fact] + public void FacadeAnotherStringPropertyChangedEventFiresOnAssignementTest() + { + var expectedPropertyChanged = "AnotherStringProperty"; + var resultPropertyChanged = string.Empty; + + var facade = new FacadeModel(new BaseModel()); + + var obj = (INotifyPropertyChanged) facade; + obj.PropertyChanged += (sender, args) => resultPropertyChanged = args.PropertyName; + + facade.AnotherStringProperty = "Some New Value"; + + Assert.Equal(expectedPropertyChanged, resultPropertyChanged); + } + + [Fact] + public void StringPropertyOnWeavedDecoratorReturnsBaseModelDefaultStringValue() + { + var model = new BaseModel(); + var expectedResult = model.StringProperty; + + var decorator = new DecoratorModel(model); + + Assert.Equal(expectedResult, decorator.StringProperty); + } + + [Fact] + public void DecoratorStringPropertyRaisesPropertyChanged() + { + var expectedPropertyChanged = "StringProperty"; + var resultPropertyChanged = string.Empty; + + var decorator = new DecoratorModel(new BaseModel()); + + var obj = (INotifyPropertyChanged) decorator; + obj.PropertyChanged += (sender, args) => resultPropertyChanged = args.PropertyName; + + decorator.StringProperty = "Some New Value"; + + Assert.Equal(expectedPropertyChanged, resultPropertyChanged); + } + + + [Fact] + public void DecoratorReactiveStringPropertyRaisesPropertyChanged() + { + var expectedPropertyChanged = "SomeCoolNewProperty"; + var resultPropertyChanged = string.Empty; + + var decorator = new DecoratorModel(new BaseModel()); + + var obj = (INotifyPropertyChanged)decorator; + obj.PropertyChanged += (sender, args) => resultPropertyChanged = args.PropertyName; + + decorator.UpdateCoolProperty("Some Cool Property"); + Assert.Equal(expectedPropertyChanged, resultPropertyChanged); + } + } + + public class BaseModel : ReactiveObject + { + public virtual int IntProperty { get; set; } = 5; + public virtual string StringProperty { get; set; } = "Initial Value"; + } + + public class FacadeModel : ReactiveObject + { + private BaseModel _dependency; + + public FacadeModel() + { + _dependency = new BaseModel(); + } + + public FacadeModel(BaseModel dependency) + { + _dependency = dependency; + } + + public BaseModel Dependency + { + get { return _dependency; } + private set { _dependency = value; } + } + + // Property with the same name, will look for a like for like name on the named dependency + [ReactiveDependency(nameof(Dependency))] + public int IntProperty { get; set; } + + // Property named differently to that on the dependency but still pass through value + [ReactiveDependency(nameof(Dependency), TargetProperty = "StringProperty")] + public string AnotherStringProperty { get; set; } + } + + public class DecoratorModel : BaseModel + { + private readonly BaseModel _model; + + // Testing ctor + public DecoratorModel() + { + _model = new BaseModel(); + } + + public DecoratorModel(BaseModel baseModel) + { + _model = baseModel; + } + + [Reactive] + public string SomeCoolNewProperty { get; set; } + + // Works with private fields + [ReactiveDependency(nameof(_model))] + public override string StringProperty { get; set; } + + // Can't be attributed as has additional functionality in the decorated get + public override int IntProperty + { + get { return _model.IntProperty * 2; } + set + { + _model.IntProperty = value; + this.RaisePropertyChanged(); + } + } + + public void UpdateCoolProperty(string coolNewProperty) + { + SomeCoolNewProperty = coolNewProperty; + } + } +} diff --git a/src/ReactiveUI.Fody.Tests/ReactiveUI.Fody.Tests.csproj b/src/ReactiveUI.Fody.Tests/ReactiveUI.Fody.Tests.csproj new file mode 100644 index 0000000000..c16fc547ee --- /dev/null +++ b/src/ReactiveUI.Fody.Tests/ReactiveUI.Fody.Tests.csproj @@ -0,0 +1,49 @@ + + + net461 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ReactiveUI.Fody.Tests/ReactiveUI.Fody.Tests.licenseheader b/src/ReactiveUI.Fody.Tests/ReactiveUI.Fody.Tests.licenseheader new file mode 100644 index 0000000000..712fdc59d7 --- /dev/null +++ b/src/ReactiveUI.Fody.Tests/ReactiveUI.Fody.Tests.licenseheader @@ -0,0 +1,12 @@ +extensions: designer.cs generated.cs +extensions: .cs +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +extensions: .xml .config .xsd + diff --git a/src/ReactiveUI.Fody/CecilExtensions.cs b/src/ReactiveUI.Fody/CecilExtensions.cs new file mode 100644 index 0000000000..e6b7b3084f --- /dev/null +++ b/src/ReactiveUI.Fody/CecilExtensions.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace ReactiveUI.Fody +{ + public static class CecilExtensions + { + public static void Emit(this MethodBody body, Action il) + { + il(body.GetILProcessor()); + } + + public static GenericInstanceMethod MakeGenericMethod(this MethodReference method, params TypeReference[] genericArguments) + { + var result = new GenericInstanceMethod(method); + foreach (var argument in genericArguments) + result.GenericArguments.Add(argument); + return result; + } + + public static bool IsAssignableFrom(this TypeReference baseType, TypeReference type, Action logger = null) + { + return baseType.Resolve().IsAssignableFrom(type.Resolve(), logger); + } + + public static bool IsAssignableFrom(this TypeDefinition baseType, TypeDefinition type, Action logger = null) + { + logger = logger ?? (x => {}); + + Queue queue = new Queue(); + queue.Enqueue(type); + + while (queue.Any()) + { + var current = queue.Dequeue(); + logger(current.FullName); + + if (baseType.FullName == current.FullName) + return true; + + if (current.BaseType != null) + queue.Enqueue(current.BaseType.Resolve()); + + foreach (var @interface in current.Interfaces) + { + queue.Enqueue(@interface.InterfaceType.Resolve()); + } + } + + return false; + } + + public static bool IsDefined(this IMemberDefinition member, TypeReference attributeType) + { + return member.HasCustomAttributes && member.CustomAttributes.Any(x => x.AttributeType.FullName == attributeType.FullName); + } + + public static MethodReference Bind(this MethodReference method, GenericInstanceType genericType) + { + var reference = new MethodReference(method.Name, method.ReturnType, genericType); + reference.HasThis = method.HasThis; + reference.ExplicitThis = method.ExplicitThis; + reference.CallingConvention = method.CallingConvention; + + foreach (var parameter in method.Parameters) + reference.Parameters.Add(new ParameterDefinition(parameter.ParameterType)); + + return reference; + } + + /* + public static MethodReference BindDefinition(this MethodReference method, TypeReference genericTypeDefinition) + { + if (!genericTypeDefinition.HasGenericParameters) + return method; + + var genericDeclaration = new GenericInstanceType(genericTypeDefinition); + foreach (var parameter in genericTypeDefinition.GenericParameters) + { + genericDeclaration.GenericArguments.Add(parameter); + } + var reference = new MethodReference(method.Name, method.ReturnType, genericDeclaration); + reference.HasThis = method.HasThis; + reference.ExplicitThis = method.ExplicitThis; + reference.CallingConvention = method.CallingConvention; + + foreach (var parameter in method.Parameters) + reference.Parameters.Add(new ParameterDefinition(parameter.ParameterType)); + + return reference; + } + */ + + public static FieldReference BindDefinition(this FieldReference field, TypeReference genericTypeDefinition) + { + if (!genericTypeDefinition.HasGenericParameters) + return field; + + var genericDeclaration = new GenericInstanceType(genericTypeDefinition); + foreach (var parameter in genericTypeDefinition.GenericParameters) + { + genericDeclaration.GenericArguments.Add(parameter); + } + var reference = new FieldReference(field.Name, field.FieldType, genericDeclaration); + return reference; + } + + public static AssemblyNameReference FindAssembly(this ModuleDefinition currentModule, string assemblyName) + { + return currentModule.AssemblyReferences.SingleOrDefault(x => x.Name == assemblyName); + } + + public static TypeReference FindType(this ModuleDefinition currentModule, string @namespace, string typeName, IMetadataScope scope = null, params string[] typeParameters) + { + var result = new TypeReference(@namespace, typeName, currentModule, scope); + foreach (var typeParameter in typeParameters) + { + result.GenericParameters.Add(new GenericParameter(typeParameter, result)); + } + return result; + } + + public static bool CompareTo(this TypeReference type, TypeReference compareTo) + { + return type.FullName == compareTo.FullName; + } + +/* + public static IEnumerable GetAllTypes(this ModuleDefinition module) + { + var stack = new Stack(); + foreach (var type in module.Types) + { + stack.Push(type); + } + while (stack.Any()) + { + var current = stack.Pop(); + yield return current; + + foreach (var nestedType in current.NestedTypes) + { + stack.Push(nestedType); + } + } + } +*/ + } +} diff --git a/src/ReactiveUI.Fody/ModuleWeaver.cs b/src/ReactiveUI.Fody/ModuleWeaver.cs new file mode 100644 index 0000000000..057099afe3 --- /dev/null +++ b/src/ReactiveUI.Fody/ModuleWeaver.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Fody; + +namespace ReactiveUI.Fody +{ + public class ModuleWeaver : BaseModuleWeaver + { + public override void Execute() + { + var propertyWeaver = new ReactiveUIPropertyWeaver + { + ModuleDefinition = ModuleDefinition, + LogInfo = LogInfo, + LogError = LogError + }; + propertyWeaver.Execute(); + + var observableAsPropertyWeaver = new ObservableAsPropertyWeaver + { + ModuleDefinition = ModuleDefinition, + LogInfo = LogInfo + }; + observableAsPropertyWeaver.Execute(); + + var reactiveDependencyWeaver = new ReactiveDependencyPropertyWeaver + { + ModuleDefinition = ModuleDefinition, + LogInfo = LogInfo, + LogError = LogError + }; + reactiveDependencyWeaver.Execute(); + } + + public override IEnumerable GetAssembliesForScanning() + { + yield return "mscorlib"; + yield return "netstandard"; + yield return "System"; + yield return "System.Runtime"; + yield return "ReactiveUI"; + yield return "ReactiveUI.Fody.Helpers"; + } + } +} diff --git a/src/ReactiveUI.Fody/ObservableAsPropertyWeaver.cs b/src/ReactiveUI.Fody/ObservableAsPropertyWeaver.cs new file mode 100644 index 0000000000..d0529d5f9f --- /dev/null +++ b/src/ReactiveUI.Fody/ObservableAsPropertyWeaver.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; + +namespace ReactiveUI.Fody +{ + public class ObservableAsPropertyWeaver + { + public ModuleDefinition ModuleDefinition { get; set; } + + // Will log an MessageImportance.High message to MSBuild. OPTIONAL + public Action LogInfo { get; set; } + + public void Execute() + { + var reactiveUI = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI").OrderByDescending(x => x.Version).FirstOrDefault(); + if (reactiveUI == null) + { + LogInfo("Could not find assembly: ReactiveUI (" + string.Join(", ", ModuleDefinition.AssemblyReferences.Select(x => x.Name)) + ")"); + return; + } + LogInfo($"{reactiveUI.Name} {reactiveUI.Version}"); + var helpers = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI.Fody.Helpers").OrderByDescending(x => x.Version).FirstOrDefault(); + if (helpers == null) + { + LogInfo("Could not find assembly: ReactiveUI.Fody.Helpers (" + string.Join(", ", ModuleDefinition.AssemblyReferences.Select(x => x.Name)) + ")"); + return; + } + LogInfo($"{helpers.Name} {helpers.Version}"); + + var reactiveObject = ModuleDefinition.FindType("ReactiveUI", "ReactiveObject", reactiveUI); + + // The types we will scan are subclasses of ReactiveObject + var targetTypes = ModuleDefinition.GetAllTypes().Where(x => x.BaseType != null && reactiveObject.IsAssignableFrom(x.BaseType)); + + var observableAsPropertyHelper = ModuleDefinition.FindType("ReactiveUI", "ObservableAsPropertyHelper`1", reactiveUI, "T"); + var observableAsPropertyAttribute = ModuleDefinition.FindType("ReactiveUI.Fody.Helpers", "ObservableAsPropertyAttribute", helpers); + var observableAsPropertyHelperGetValue = ModuleDefinition.ImportReference(observableAsPropertyHelper.Resolve().Properties.Single(x => x.Name == "Value").GetMethod); + var systemRuntimeName = ModuleDefinition.AssemblyReferences.FirstOrDefault(x => x.Name == "System.Runtime"); + var exceptionDefinition = systemRuntimeName == null + ? ModuleDefinition.ImportReference(typeof(Exception)).Resolve() // Referenced from .NET Framework + : ModuleDefinition.AssemblyResolver.Resolve(systemRuntimeName).MainModule.Types.First(x => x.Name == "Exception"); // Referenced from .NET Standard + var constructorDefinition = exceptionDefinition.GetConstructors().Single(x => x.Parameters.Count == 1); + var exceptionConstructor = ModuleDefinition.ImportReference(constructorDefinition); + + foreach (var targetType in targetTypes) + { + foreach (var property in targetType.Properties.Where(x => x.IsDefined(observableAsPropertyAttribute) || (x.GetMethod?.IsDefined(observableAsPropertyAttribute) ?? false)).ToArray()) + { + var genericObservableAsPropertyHelper = observableAsPropertyHelper.MakeGenericInstanceType(property.PropertyType); + var genericObservableAsPropertyHelperGetValue = observableAsPropertyHelperGetValue.Bind(genericObservableAsPropertyHelper); + ModuleDefinition.ImportReference(genericObservableAsPropertyHelperGetValue); + + // Declare a field to store the property value + var field = new FieldDefinition("$" + property.Name, FieldAttributes.Private, genericObservableAsPropertyHelper); + targetType.Fields.Add(field); + + // It's an auto-property, so remove the generated field + if (property.SetMethod != null && property.SetMethod.HasBody) + { + // Remove old field (the generated backing field for the auto property) + var oldField = (FieldReference)property.GetMethod.Body.Instructions.Where(x => x.Operand is FieldReference).Single().Operand; + var oldFieldDefinition = oldField.Resolve(); + targetType.Fields.Remove(oldFieldDefinition); + + // Re-implement setter to throw an exception + property.SetMethod.Body = new MethodBody(property.SetMethod); + property.SetMethod.Body.Emit(il => + { + il.Emit(OpCodes.Ldstr, "Never call the setter of an ObservabeAsPropertyHelper property."); + il.Emit(OpCodes.Newobj, exceptionConstructor); + il.Emit(OpCodes.Throw); + il.Emit(OpCodes.Ret); + }); + } + + property.GetMethod.Body = new MethodBody(property.GetMethod); + property.GetMethod.Body.Emit(il => + { + var isValid = il.Create(OpCodes.Nop); + il.Emit(OpCodes.Ldarg_0); // this + il.Emit(OpCodes.Ldfld, field.BindDefinition(targetType)); // pop -> this.$PropertyName + il.Emit(OpCodes.Dup); // Put an extra copy of this.$PropertyName onto the stack + il.Emit(OpCodes.Brtrue, isValid); // If the helper is null, return the default value for the property + il.Emit(OpCodes.Pop); // Drop this.$PropertyName + EmitDefaultValue(property.GetMethod.Body, il, property.PropertyType); // Put the default value onto the stack + il.Emit(OpCodes.Ret); // Return that default value + il.Append(isValid); // Add a marker for if the helper is not null + il.Emit(OpCodes.Callvirt, genericObservableAsPropertyHelperGetValue); // pop -> this.$PropertyName.Value + il.Emit(OpCodes.Ret); // Return the value that is on the stack + }); + } + } + } + + public void EmitDefaultValue(MethodBody methodBody, ILProcessor il, TypeReference type) + { + if (type.CompareTo(ModuleDefinition.TypeSystem.Boolean) || type.CompareTo(ModuleDefinition.TypeSystem.Byte) || + type.CompareTo(ModuleDefinition.TypeSystem.Int16) || type.CompareTo(ModuleDefinition.TypeSystem.Int32)) + { + il.Emit(OpCodes.Ldc_I4_0); + } + else if (type.CompareTo(ModuleDefinition.TypeSystem.Single)) + { + il.Emit(OpCodes.Ldc_R4, (float)0); + } + else if (type.CompareTo(ModuleDefinition.TypeSystem.Int64)) + { + il.Emit(OpCodes.Ldc_I8); + } + else if (type.CompareTo(ModuleDefinition.TypeSystem.Double)) + { + il.Emit(OpCodes.Ldc_R8, (double)0); + } + else if (type.IsGenericParameter || type.IsValueType) + { + methodBody.InitLocals = true; + var local = new VariableDefinition(type); + il.Body.Variables.Add(local); + il.Emit(OpCodes.Ldloca_S, local); + il.Emit(OpCodes.Initobj, type); + il.Emit(OpCodes.Ldloc, local); + } + else + { + il.Emit(OpCodes.Ldnull); + } + } + } +} diff --git a/src/ReactiveUI.Fody/Properties/AssemblyInfo.cs b/src/ReactiveUI.Fody/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..4fa29309e5 --- /dev/null +++ b/src/ReactiveUI.Fody/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ReactiveUI.Tests")] +[assembly: InternalsVisibleTo("ReactiveUI.Winforms")] +[assembly: InternalsVisibleTo("ReactiveUI.Wpf")] +[assembly: InternalsVisibleTo("ReactiveUI.XamForms")] +[assembly: InternalsVisibleTo("ReactiveUI.AndroidSupport")] diff --git a/src/ReactiveUI.Fody/ReactiveDependencyPropertyWeaver.cs b/src/ReactiveUI.Fody/ReactiveDependencyPropertyWeaver.cs new file mode 100644 index 0000000000..fe39456487 --- /dev/null +++ b/src/ReactiveUI.Fody/ReactiveDependencyPropertyWeaver.cs @@ -0,0 +1,191 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; + +namespace ReactiveUI.Fody +{ + public class ReactiveDependencyPropertyWeaver + { + public ModuleDefinition ModuleDefinition { get; set; } + + // Will log an MessageImportance.High message to MSBuild. OPTIONAL + public Action LogInfo { get; set; } + + // Will log an error message to MSBuild. OPTIONAL + public Action LogError { get; set; } + + public void Execute() + { + var reactiveUI = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI").OrderByDescending(x => x.Version).FirstOrDefault(); + if (reactiveUI == null) + { + LogInfo("Could not find assembly: ReactiveUI (" + string.Join(", ", ModuleDefinition.AssemblyReferences.Select(x => x.Name)) + ")"); + return; + } + LogInfo($"{reactiveUI.Name} {reactiveUI.Version}"); + var helpers = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI.Fody.Helpers").OrderByDescending(x => x.Version).FirstOrDefault(); + if (helpers == null) + { + LogInfo("Could not find assembly: ReactiveUI.Fody.Helpers (" + string.Join(", ", ModuleDefinition.AssemblyReferences.Select(x => x.Name)) + ")"); + return; + } + LogInfo($"{helpers.Name} {helpers.Version}"); + var reactiveObject = new TypeReference("ReactiveUI", "IReactiveObject", ModuleDefinition, reactiveUI); + + var targetTypes = ModuleDefinition.GetAllTypes().Where(x => x.BaseType != null && reactiveObject.IsAssignableFrom(x.BaseType)).ToArray(); + var reactiveObjectExtensions = new TypeReference("ReactiveUI", "IReactiveObjectExtensions", ModuleDefinition, reactiveUI).Resolve(); + if (reactiveObjectExtensions == null) throw new Exception("reactiveObjectExtensions is null"); + + var raisePropertyChangedMethod = ModuleDefinition.Import(reactiveObjectExtensions.Methods.Single(x => x.Name == "RaisePropertyChanged")); + if (raisePropertyChangedMethod == null) throw new Exception("raisePropertyChangedMethod is null"); + + var reactiveDependencyAttribute = ModuleDefinition.FindType("ReactiveUI.Fody.Helpers", "ReactiveDependencyAttribute", helpers); + if (reactiveDependencyAttribute == null) throw new Exception("reactiveDecoratorAttribute is null"); + + foreach (var targetType in targetTypes.Where(x => x.Properties.Any(y => y.IsDefined(reactiveDependencyAttribute))).ToArray()) + { + foreach (var facadeProperty in targetType.Properties.Where(x => x.IsDefined(reactiveDependencyAttribute)).ToArray()) + { + // If the property already has a body then do not weave to prevent loss of instructions + if (!facadeProperty.GetMethod.Body.Instructions.Any(x => x.Operand is FieldReference) || facadeProperty.GetMethod.Body.HasVariables) + { + LogError($"Property {facadeProperty.Name} is not an auto property and therefore not suitable for ReactiveDependency weaving"); + continue; + } + + var attribute = facadeProperty.CustomAttributes.First(x => x.AttributeType.FullName == reactiveDependencyAttribute.FullName); + + var targetNamedArgument = attribute.ConstructorArguments.FirstOrDefault(); + var targetValue = targetNamedArgument.Value?.ToString(); + if (string.IsNullOrEmpty(targetValue)) + { + LogError("No target property defined on the object"); + continue; + } + + if (targetType.Properties.All(x => x.Name != targetValue) && targetType.Fields.All(x => x.Name != targetValue)) + { + LogError($"dependency object property/field name '{targetValue}' not found on target type {targetType.Name}"); + continue; + } + + var objPropertyTarget = targetType.Properties.FirstOrDefault(x => x.Name == targetValue); + var objFieldTarget = targetType.Fields.FirstOrDefault(x => x.Name == targetValue); + + var objDependencyTargetType = objPropertyTarget != null + ? objPropertyTarget.PropertyType.Resolve() + : objFieldTarget?.FieldType.Resolve(); + + if(objDependencyTargetType == null) + { + LogError("Couldn't result the dependency type"); + continue; + } + + // Look for the target property on the member obj + var destinationPropertyNamedArgument = attribute.Properties.FirstOrDefault(x => x.Name == "TargetProperty"); + var destinationPropertyName = destinationPropertyNamedArgument.Argument.Value?.ToString(); + + // If no target property was specified use this property's name as the target on the decorated object (ala a decorated property) + if (string.IsNullOrEmpty(destinationPropertyName)) destinationPropertyName = facadeProperty.Name; + + if (objDependencyTargetType.Properties.All(x => x.Name != destinationPropertyName)) + { + LogError($"Target property {destinationPropertyName} on dependency of type {objDependencyTargetType.DeclaringType.Name} not found"); + continue; + } + + var destinationProperty = objDependencyTargetType.Properties.First(x => x.Name == destinationPropertyName); + + // The property on the facade/decorator should have a setter + if (facadeProperty.SetMethod == null) + { + LogError($"Property {facadeProperty.DeclaringType.FullName}.{facadeProperty.Name} has no setter, therefore it is not possible for the property to change, and thus should not be marked with [ReactiveDecorator]"); + continue; + } + + // The property on the dependency should have a setter e.g. Dependency.SomeProperty = value; + if (destinationProperty.SetMethod == null) + { + LogError($"Dependency object's property {destinationProperty.DeclaringType.FullName}.{destinationProperty.Name} has no setter, therefore it is not possible for the property to change, and thus should not be marked with [ReactiveDecorator]"); + continue; + } + + // Remove old field (the generated backing field for the auto property) + var oldField = (FieldReference)facadeProperty.GetMethod.Body.Instructions.Where(x => x.Operand is FieldReference).Single().Operand; + var oldFieldDefinition = oldField.Resolve(); + targetType.Fields.Remove(oldFieldDefinition); + + // See if there exists an initializer for the auto-property + var constructors = targetType.Methods.Where(x => x.IsConstructor); + foreach (var constructor in constructors) + { + var fieldAssignment = constructor.Body.Instructions.SingleOrDefault(x => Equals(x.Operand, oldFieldDefinition) || Equals(x.Operand, oldField)); + if (fieldAssignment != null) + { + // Replace field assignment with a property set (the stack semantics are the same for both, + // so happily we don't have to manipulate the bytecode any further.) + var setterCall = constructor.Body.GetILProcessor().Create(facadeProperty.SetMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, facadeProperty.SetMethod); + constructor.Body.GetILProcessor().Replace(fieldAssignment, setterCall); + } + } + + // Build out the getter which simply returns the value of the generated field + facadeProperty.GetMethod.Body = new MethodBody(facadeProperty.GetMethod); + facadeProperty.GetMethod.Body.Emit(il => + { + il.Emit(OpCodes.Ldarg_0); // this + if (objPropertyTarget != null) + { + il.Emit(objPropertyTarget.GetMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, objPropertyTarget.GetMethod); + } + else + { + il.Emit(OpCodes.Ldfld, objFieldTarget); + } + il.Emit(destinationProperty.GetMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, destinationProperty.GetMethod); + il.Emit(OpCodes.Ret); + }); + + TypeReference genericTargetType = targetType; + if (targetType.HasGenericParameters) + { + var genericDeclaration = new GenericInstanceType(targetType); + foreach (var parameter in targetType.GenericParameters) + { + genericDeclaration.GenericArguments.Add(parameter); + } + genericTargetType = genericDeclaration; + } + + var methodReference = raisePropertyChangedMethod.MakeGenericMethod(genericTargetType); + facadeProperty.SetMethod.Body = new MethodBody(facadeProperty.SetMethod); + facadeProperty.SetMethod.Body.Emit(il => + { + il.Emit(OpCodes.Ldarg_0); + if (objPropertyTarget != null) + { + il.Emit(objPropertyTarget.GetMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, objPropertyTarget.GetMethod); + } + else + { + il.Emit(OpCodes.Ldfld, objFieldTarget); + } + il.Emit(OpCodes.Ldarg_1); + il.Emit(destinationProperty.SetMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, destinationProperty.SetMethod); // Set the nested property + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, facadeProperty.Name); // "PropertyName" + il.Emit(OpCodes.Call, methodReference); // this.RaisePropertyChanged("PropertyName") + il.Emit(OpCodes.Ret); + }); + } + } + } + } +} diff --git a/src/ReactiveUI.Fody/ReactiveUI.Fody.csproj b/src/ReactiveUI.Fody/ReactiveUI.Fody.csproj new file mode 100644 index 0000000000..816b50063f --- /dev/null +++ b/src/ReactiveUI.Fody/ReactiveUI.Fody.csproj @@ -0,0 +1,13 @@ + + + netstandard2.0;net461 + ReactiveUI.Fody + ReactiveUI.Fody + Fody Weavers for ReactiveUI.Fody. + False + + + + + + \ No newline at end of file diff --git a/src/ReactiveUI.Fody/ReactiveUI.Fody.licenseheader b/src/ReactiveUI.Fody/ReactiveUI.Fody.licenseheader new file mode 100644 index 0000000000..712fdc59d7 --- /dev/null +++ b/src/ReactiveUI.Fody/ReactiveUI.Fody.licenseheader @@ -0,0 +1,12 @@ +extensions: designer.cs generated.cs +extensions: .cs +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +extensions: .xml .config .xsd + diff --git a/src/ReactiveUI.Fody/ReactiveUIPropertyWeaver.cs b/src/ReactiveUI.Fody/ReactiveUIPropertyWeaver.cs new file mode 100644 index 0000000000..314e155eff --- /dev/null +++ b/src/ReactiveUI.Fody/ReactiveUIPropertyWeaver.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; + +namespace ReactiveUI.Fody +{ + /// + /// Weaver that replaces properties marked with `[DataMember]` on subclasses of `ReactiveObject` with an + /// implementation that invokes `RaisePropertyChanged` as is required for reaciveui. + /// + public class ReactiveUIPropertyWeaver + { + public ModuleDefinition ModuleDefinition { get; set; } + + // Will log an MessageImportance.High message to MSBuild. OPTIONAL + public Action LogInfo { get; set; } + + // Will log an error message to MSBuild. OPTIONAL + public Action LogError { get; set; } + + public void Execute() + { + var reactiveUI = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI").OrderByDescending(x => x.Version).FirstOrDefault(); + if (reactiveUI == null) + { + LogInfo("Could not find assembly: ReactiveUI (" + string.Join(", ", ModuleDefinition.AssemblyReferences.Select(x => x.Name)) + ")"); + return; + } + LogInfo($"{reactiveUI.Name} {reactiveUI.Version}"); + var helpers = ModuleDefinition.AssemblyReferences.Where(x => x.Name == "ReactiveUI.Fody.Helpers").OrderByDescending(x => x.Version).FirstOrDefault(); + if (helpers == null) + { + LogInfo("Could not find assembly: ReactiveUI.Fody.Helpers (" + string.Join(", ", ModuleDefinition.AssemblyReferences.Select(x => x.Name)) + ")"); + return; + } + LogInfo($"{helpers.Name} {helpers.Version}"); + var reactiveObject = new TypeReference("ReactiveUI", "IReactiveObject", ModuleDefinition, reactiveUI); + var targetTypes = ModuleDefinition.GetAllTypes().Where(x => x.BaseType != null && reactiveObject.IsAssignableFrom(x.BaseType)).ToArray(); + var reactiveObjectExtensions = new TypeReference("ReactiveUI", "IReactiveObjectExtensions", ModuleDefinition, reactiveUI).Resolve(); + if (reactiveObjectExtensions == null) + throw new Exception("reactiveObjectExtensions is null"); + + var raiseAndSetIfChangedMethod = ModuleDefinition.Import(reactiveObjectExtensions.Methods.Single(x => x.Name == "RaiseAndSetIfChanged")); + if (raiseAndSetIfChangedMethod == null) + throw new Exception("raiseAndSetIfChangedMethod is null"); + + var reactiveAttribute = ModuleDefinition.FindType("ReactiveUI.Fody.Helpers", "ReactiveAttribute", helpers); + if (reactiveAttribute == null) + throw new Exception("reactiveAttribute is null"); + + foreach (var targetType in targetTypes) + { + foreach (var property in targetType.Properties.Where(x => x.IsDefined(reactiveAttribute)).ToArray()) + { + if (property.SetMethod == null) + { + LogError($"Property {property.DeclaringType.FullName}.{property.Name} has no setter, therefore it is not possible for the property to change, and thus should not be marked with [Reactive]"); + continue; + } + + // Declare a field to store the property value + var field = new FieldDefinition("$" + property.Name, FieldAttributes.Private, property.PropertyType); + targetType.Fields.Add(field); + + // Remove old field (the generated backing field for the auto property) + var oldField = (FieldReference)property.GetMethod.Body.Instructions.Where(x => x.Operand is FieldReference).Single().Operand; + var oldFieldDefinition = oldField.Resolve(); + targetType.Fields.Remove(oldFieldDefinition); + + // See if there exists an initializer for the auto-property + var constructors = targetType.Methods.Where(x => x.IsConstructor); + foreach (var constructor in constructors) + { + var fieldAssignment = constructor.Body.Instructions.SingleOrDefault(x => Equals(x.Operand, oldFieldDefinition) || Equals(x.Operand, oldField)); + if (fieldAssignment != null) + { + // Replace field assignment with a property set (the stack semantics are the same for both, + // so happily we don't have to manipulate the bytecode any further.) + var setterCall = constructor.Body.GetILProcessor().Create(property.SetMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, property.SetMethod); + constructor.Body.GetILProcessor().Replace(fieldAssignment, setterCall); + } + } + + // Build out the getter which simply returns the value of the generated field + property.GetMethod.Body = new MethodBody(property.GetMethod); + property.GetMethod.Body.Emit(il => + { + il.Emit(OpCodes.Ldarg_0); // this + il.Emit(OpCodes.Ldfld, field.BindDefinition(targetType)); // pop -> this.$PropertyName + il.Emit(OpCodes.Ret); // Return the field value that is lying on the stack + }); + + TypeReference genericTargetType = targetType; + if (targetType.HasGenericParameters) + { + var genericDeclaration = new GenericInstanceType(targetType); + foreach (var parameter in targetType.GenericParameters) + { + genericDeclaration.GenericArguments.Add(parameter); + } + genericTargetType = genericDeclaration; + } + + var methodReference = raiseAndSetIfChangedMethod.MakeGenericMethod(genericTargetType, property.PropertyType); + + // Build out the setter which fires the RaiseAndSetIfChanged method + if (property.SetMethod == null) + { + throw new Exception("[Reactive] is decorating " + property.DeclaringType.FullName + "." + property.Name + ", but the property has no setter so there would be nothing to react to. Consider removing the attribute."); + } + property.SetMethod.Body = new MethodBody(property.SetMethod); + property.SetMethod.Body.Emit(il => + { + il.Emit(OpCodes.Ldarg_0); // this + il.Emit(OpCodes.Ldarg_0); // this + il.Emit(OpCodes.Ldflda, field.BindDefinition(targetType)); // pop -> this.$PropertyName + il.Emit(OpCodes.Ldarg_1); // value + il.Emit(OpCodes.Ldstr, property.Name); // "PropertyName" + il.Emit(OpCodes.Call, methodReference); // pop * 4 -> this.RaiseAndSetIfChanged(this.$PropertyName, value, "PropertyName") + il.Emit(OpCodes.Pop); // We don't care about the result of RaiseAndSetIfChanged, so pop it off the stack (stack is now empty) + il.Emit(OpCodes.Ret); // Return out of the function + }); + } + } + } + } +} \ No newline at end of file diff --git a/src/ReactiveUI.Tests/ReactiveUI.Tests.csproj b/src/ReactiveUI.Tests/ReactiveUI.Tests.csproj index 01604a5583..c5a071766a 100644 --- a/src/ReactiveUI.Tests/ReactiveUI.Tests.csproj +++ b/src/ReactiveUI.Tests/ReactiveUI.Tests.csproj @@ -15,7 +15,6 @@ - diff --git a/src/ReactiveUI.sln b/src/ReactiveUI.sln index 593c21d278..8c83170dfd 100644 --- a/src/ReactiveUI.sln +++ b/src/ReactiveUI.sln @@ -28,6 +28,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Wpf", "ReactiveU EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.LeakTests", "ReactiveUI.LeakTests\ReactiveUI.LeakTests.csproj", "{1AC71A71-F5F3-4F96-BDA9-A9DC7F572DB9}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Fody", "ReactiveUI.Fody\ReactiveUI.Fody.csproj", "{7DE43BB9-5AC8-446A-8D8B-88C9201D802E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Fody.Helpers", "ReactiveUI.Fody.Helpers\ReactiveUI.Fody.Helpers.csproj", "{20750BB4-36DD-4F8C-B970-D7809810EC98}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Fody.Tests", "ReactiveUI.Fody.Tests\ReactiveUI.Fody.Tests.csproj", "{404B0F3F-7343-4E54-A863-F27B99FE788B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -524,6 +530,174 @@ Global {1AC71A71-F5F3-4F96-BDA9-A9DC7F572DB9}.Release|x64.Build.0 = Release|Any CPU {1AC71A71-F5F3-4F96-BDA9-A9DC7F572DB9}.Release|x86.ActiveCfg = Release|Any CPU {1AC71A71-F5F3-4F96-BDA9-A9DC7F572DB9}.Release|x86.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|ARM.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|Mixed Platforms.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|Mixed Platforms.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|x64.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Ad-Hoc|x86.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|Any CPU.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|ARM.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|ARM.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|iPhone.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|Mixed Platforms.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|Mixed Platforms.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|x64.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|x64.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|x86.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.AppStore|x86.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|ARM.ActiveCfg = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|ARM.Build.0 = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|iPhone.Build.0 = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|x64.ActiveCfg = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|x64.Build.0 = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|x86.ActiveCfg = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Debug|x86.Build.0 = Debug|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|Any CPU.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|ARM.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|ARM.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|iPhone.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|iPhone.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|x64.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|x64.Build.0 = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|x86.ActiveCfg = Release|Any CPU + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E}.Release|x86.Build.0 = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|Mixed Platforms.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|Mixed Platforms.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|ARM.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|iPhone.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|Mixed Platforms.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|Mixed Platforms.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|x64.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|x64.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|x86.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.AppStore|x86.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|ARM.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|ARM.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|iPhone.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|x64.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|x64.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|x86.ActiveCfg = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Debug|x86.Build.0 = Debug|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|Any CPU.Build.0 = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|ARM.ActiveCfg = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|ARM.Build.0 = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|iPhone.ActiveCfg = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|iPhone.Build.0 = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|x64.ActiveCfg = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|x64.Build.0 = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|x86.ActiveCfg = Release|Any CPU + {20750BB4-36DD-4F8C-B970-D7809810EC98}.Release|x86.Build.0 = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|Mixed Platforms.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|Mixed Platforms.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|ARM.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|iPhone.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|Mixed Platforms.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|Mixed Platforms.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|x64.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|x64.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|x86.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.AppStore|x86.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|ARM.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|ARM.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|iPhone.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|x64.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|x64.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|x86.ActiveCfg = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Debug|x86.Build.0 = Debug|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|Any CPU.Build.0 = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|ARM.ActiveCfg = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|ARM.Build.0 = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|iPhone.ActiveCfg = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|iPhone.Build.0 = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|x64.ActiveCfg = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|x64.Build.0 = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|x86.ActiveCfg = Release|Any CPU + {404B0F3F-7343-4E54-A863-F27B99FE788B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE