diff --git a/README.md b/README.md index a27ddaa..2da60dd 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,8 @@ public long Counter => this.GetFromObservable(Observable.Timer( ``` > 💡 If you use [Chinook Snippets](https://marketplace.visualstudio.com/items?itemName=nventivecorp.ChinookSnippets), you can quickly generate a property from observable using the snippets `"ckpropo"` (**c**hinoo**k** **prop**erty from **o**bservable) or `"ckpropog"` (**c**hinoo**k** **prop**erty from **o**bservable **g**et-only). +> 💡 Consider using the overload that takes a `Func>` to avoid evaluating the observable every time the property is read and potentially save some memory allocations. + ### Create properties from `Task` Using `IViewModel.GetFromTask`, you can create a property that updates itself based on a `Task` result. ```csharp diff --git a/src/DynamicMvvm.Abstractions/ViewModel/IViewModel.Extensions.Properties.cs b/src/DynamicMvvm.Abstractions/ViewModel/IViewModel.Extensions.Properties.cs index 4afb822..15310bb 100644 --- a/src/DynamicMvvm.Abstractions/ViewModel/IViewModel.Extensions.Properties.cs +++ b/src/DynamicMvvm.Abstractions/ViewModel/IViewModel.Extensions.Properties.cs @@ -243,6 +243,41 @@ void OnDynamicPropertyChanged(IDynamicProperty dynamicProperty) } } + /// + /// Gets or creates a attached to this . + /// This overload uses a to avoid evaluating the observable sequence more than once (which can avoid memory allocations). + /// + /// The property type. + /// The owning the property. + /// The function to provide the observable of values that feeds the property. + /// The property's initial value. + /// The property's name. + /// The property's value. + public static T GetFromObservable( + this IViewModel viewModel, + Func> sourceProvider, + T initialValue = default, + [CallerMemberName] string name = null + ) + { + // We don't use GetOrCreateDynamicProperty internally to avoid the performance costs of the lambda and closure. + if (viewModel.IsDisposed) + { + return default(T); + } + + if (viewModel.TryGetDisposable(name, out var property)) + { + return viewModel.Get(property); + } + else + { + property = AddDynamicPropertyFromObservable(viewModel, sourceProvider(), initialValue, name); + + return (T)property.Value; + } + } + /// /// Sets the value of a property. /// diff --git a/src/DynamicMvvm.Tests/Integration/IntegrationTests.cs b/src/DynamicMvvm.Tests/Integration/IntegrationTests.cs index 5cae8ed..e2bd479 100644 --- a/src/DynamicMvvm.Tests/Integration/IntegrationTests.cs +++ b/src/DynamicMvvm.Tests/Integration/IntegrationTests.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; -using Chinook.DynamicMvvm; using Chinook.DynamicMvvm.Tests.Helpers; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; @@ -137,6 +137,23 @@ public void Disposing_a_child_removes_it_from_the_parent() viewModel.TryGetDisposable(nameof(viewModel.Child), out var _).Should().BeFalse(); } + [Fact] + public void GetFromObservable_WithFuncOverload_doesnt_build_observable_more_than_once() + { + var sut = new TestVM2(_serviceProvider); + + // InvocationCount should be 0 because the observable is not built until the first time it is accessed. + sut.InvocationCount.Should().Be(0); + sut.Count.Should().Be(0); + + // InvocationCount should be 1 because the observable is built the first time it is accessed. + sut.InvocationCount.Should().Be(1); + sut.Count.Should().Be(0); + + // InvocationCount should still be 1 because the property is cached. + sut.InvocationCount.Should().Be(1); + } + private class MyViewModel : ViewModelBase { public MyViewModel(IServiceProvider serviceProvider) @@ -226,5 +243,23 @@ public int MyNumber set => this.Set(value); } } + + public class TestVM2 : ViewModelBase + { + public TestVM2(IServiceProvider serviceProvider) + : base(serviceProvider: serviceProvider) + { + } + + public int InvocationCount { get; private set; } + + public int Count => this.GetFromObservable(GetObservable, initialValue: 0); + + private IObservable GetObservable() + { + ++InvocationCount; + return Observable.Never(); + } + } } }