Skip to content

Commit

Permalink
feat: Add an overload of GetFromObservable that uses a func to avoid …
Browse files Browse the repository at this point in the history
…wasteful materializations of observable chains.
  • Loading branch information
jeanplevesque committed Feb 9, 2024
1 parent 8f59a7b commit 5917cdf
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IObservable<T>>` to avoid evaluating the observable every time the property is read and potentially save some memory allocations.

### Create properties from `Task<T>`
Using `IViewModel.GetFromTask`, you can create a property that updates itself based on a `Task<T>` result.
```csharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,41 @@ void OnDynamicPropertyChanged(IDynamicProperty dynamicProperty)
}
}

/// <summary>
/// Gets or creates a <see cref="IDynamicProperty"/> attached to this <see cref="IViewModel"/>.
/// This overload uses a <see cref="Func{TResult}"/> to avoid evaluating the observable sequence more than once (which can avoid memory allocations).
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="viewModel">The <see cref="IViewModel"/> owning the property.</param>
/// <param name="sourceProvider">The function to provide the observable of values that feeds the property.</param>
/// <param name="initialValue">The property's initial value.</param>
/// <param name="name">The property's name.</param>
/// <returns>The property's value.</returns>
public static T GetFromObservable<T>(
this IViewModel viewModel,
Func<IObservable<T>> 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<IDynamicProperty>(name, out var property))
{
return viewModel.Get<T>(property);
}
else
{
property = AddDynamicPropertyFromObservable(viewModel, sourceProvider(), initialValue, name);

return (T)property.Value;
}
}

/// <summary>
/// Sets the value of a property.
/// </summary>
Expand Down
37 changes: 36 additions & 1 deletion src/DynamicMvvm.Tests/Integration/IntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<int>(GetObservable, initialValue: 0);

private IObservable<int> GetObservable()
{
++InvocationCount;
return Observable.Never<int>();
}
}
}
}

0 comments on commit 5917cdf

Please sign in to comment.