Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add an overload of GetFromObservable that uses a func #100

Merged
merged 1 commit into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>();
}
}
}
}