Skip to content

Commit 1383eec

Browse files
Merge pull request #3229 from Sergio0694/feature/mvvm-apis
Microsoft.Toolkit.Mvvm package
2 parents 3e4ba97 + ffcc807 commit 1383eec

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+5259
-2
lines changed

Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs

Lines changed: 421 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#pragma warning disable SA1512
6+
7+
// This file is inspired from the MvvmLight libray (lbugnion/mvvmlight),
8+
// more info in ThirdPartyNotices.txt in the root of the project.
9+
10+
using System;
11+
using System.Collections.Generic;
12+
using System.Runtime.CompilerServices;
13+
using Microsoft.Toolkit.Mvvm.Messaging;
14+
using Microsoft.Toolkit.Mvvm.Messaging.Messages;
15+
16+
namespace Microsoft.Toolkit.Mvvm.ComponentModel
17+
{
18+
/// <summary>
19+
/// A base class for observable objects that also acts as recipients for messages. This class is an extension of
20+
/// <see cref="ObservableObject"/> which also provides built-in support to use the <see cref="IMessenger"/> type.
21+
/// </summary>
22+
public abstract class ObservableRecipient : ObservableObject
23+
{
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="ObservableRecipient"/> class.
26+
/// </summary>
27+
/// <remarks>
28+
/// This constructor will produce an instance that will use the <see cref="Messaging.Messenger.Default"/> instance
29+
/// to perform requested operations. It will also be available locally through the <see cref="Messenger"/> property.
30+
/// </remarks>
31+
protected ObservableRecipient()
32+
: this(Messaging.Messenger.Default)
33+
{
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="ObservableRecipient"/> class.
38+
/// </summary>
39+
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send messages.</param>
40+
protected ObservableRecipient(IMessenger messenger)
41+
{
42+
Messenger = messenger;
43+
}
44+
45+
/// <summary>
46+
/// Gets the <see cref="IMessenger"/> instance in use.
47+
/// </summary>
48+
protected IMessenger Messenger { get; }
49+
50+
private bool isActive;
51+
52+
/// <summary>
53+
/// Gets or sets a value indicating whether the current view model is currently active.
54+
/// </summary>
55+
public bool IsActive
56+
{
57+
get => this.isActive;
58+
set
59+
{
60+
if (SetProperty(ref this.isActive, value, true))
61+
{
62+
if (value)
63+
{
64+
OnActivated();
65+
}
66+
else
67+
{
68+
OnDeactivated();
69+
}
70+
}
71+
}
72+
}
73+
74+
/// <summary>
75+
/// Raised whenever the <see cref="IsActive"/> property is set to <see langword="true"/>.
76+
/// Use this method to register to messages and do other initialization for this instance.
77+
/// </summary>
78+
/// <remarks>
79+
/// The base implementation registers all messages for this recipients that have been declared
80+
/// explicitly through the <see cref="IRecipient{TMessage}"/> interface, using the default channel.
81+
/// For more details on how this works, see the <see cref="MessengerExtensions.RegisterAll"/> method.
82+
/// If you need more fine tuned control, want to register messages individually or just prefer
83+
/// the lambda-style syntax for message registration, override this method and register manually.
84+
/// </remarks>
85+
protected virtual void OnActivated()
86+
{
87+
Messenger.RegisterAll(this);
88+
}
89+
90+
/// <summary>
91+
/// Raised whenever the <see cref="IsActive"/> property is set to <see langword="false"/>.
92+
/// Use this method to unregister from messages and do general cleanup for this instance.
93+
/// </summary>
94+
/// <remarks>
95+
/// The base implementation unregisters all messages for this recipient. It does so by
96+
/// invoking <see cref="IMessenger.UnregisterAll"/>, which removes all registered
97+
/// handlers for a given subscriber, regardless of what token was used to register them.
98+
/// That is, all registered handlers across all subscription channels will be removed.
99+
/// </remarks>
100+
protected virtual void OnDeactivated()
101+
{
102+
Messenger.UnregisterAll(this);
103+
}
104+
105+
/// <summary>
106+
/// Broadcasts a <see cref="PropertyChangedMessage{T}"/> with the specified
107+
/// parameters, without using any particular token (so using the default channel).
108+
/// </summary>
109+
/// <typeparam name="T">The type of the property that changed.</typeparam>
110+
/// <param name="oldValue">The value of the property before it changed.</param>
111+
/// <param name="newValue">The value of the property after it changed.</param>
112+
/// <param name="propertyName">The name of the property that changed.</param>
113+
/// <remarks>
114+
/// You should override this method if you wish to customize the channel being
115+
/// used to send the message (eg. if you need to use a specific token for the channel).
116+
/// </remarks>
117+
protected virtual void Broadcast<T>(T oldValue, T newValue, string? propertyName)
118+
{
119+
var message = new PropertyChangedMessage<T>(this, propertyName, oldValue, newValue);
120+
121+
Messenger.Send(message);
122+
}
123+
124+
/// <summary>
125+
/// Compares the current and new values for a given property. If the value has changed,
126+
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
127+
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
128+
/// </summary>
129+
/// <typeparam name="T">The type of the property that changed.</typeparam>
130+
/// <param name="field">The field storing the property's value.</param>
131+
/// <param name="newValue">The property's value after the change occurred.</param>
132+
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
133+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
134+
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
135+
/// <remarks>
136+
/// This method is just like <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/>, just with the addition
137+
/// of the <paramref name="broadcast"/> parameter. As such, following the behavior of the base method,
138+
/// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
139+
/// are not raised if the current and new value for the target property are the same.
140+
/// </remarks>
141+
protected bool SetProperty<T>(ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null)
142+
{
143+
return SetProperty(ref field, newValue, EqualityComparer<T>.Default, broadcast, propertyName);
144+
}
145+
146+
/// <summary>
147+
/// Compares the current and new values for a given property. If the value has changed,
148+
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
149+
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
150+
/// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,bool,string)"/>.
151+
/// </summary>
152+
/// <typeparam name="T">The type of the property that changed.</typeparam>
153+
/// <param name="field">The field storing the property's value.</param>
154+
/// <param name="newValue">The property's value after the change occurred.</param>
155+
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
156+
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
157+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
158+
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
159+
protected bool SetProperty<T>(ref T field, T newValue, IEqualityComparer<T> comparer, bool broadcast, [CallerMemberName] string? propertyName = null)
160+
{
161+
if (!broadcast)
162+
{
163+
return SetProperty(ref field, newValue, comparer, propertyName);
164+
}
165+
166+
T oldValue = field;
167+
168+
if (SetProperty(ref field, newValue, comparer, propertyName))
169+
{
170+
Broadcast(oldValue, newValue, propertyName);
171+
172+
return true;
173+
}
174+
175+
return false;
176+
}
177+
178+
/// <summary>
179+
/// Compares the current and new values for a given property. If the value has changed,
180+
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
181+
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event. Similarly to
182+
/// the <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/> method, this overload should only be
183+
/// used when <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/> can't be used directly.
184+
/// </summary>
185+
/// <typeparam name="T">The type of the property that changed.</typeparam>
186+
/// <param name="oldValue">The current property value.</param>
187+
/// <param name="newValue">The property's value after the change occurred.</param>
188+
/// <param name="callback">A callback to invoke to update the property value.</param>
189+
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
190+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
191+
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
192+
/// <remarks>
193+
/// This method is just like <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/>, just with the addition
194+
/// of the <paramref name="broadcast"/> parameter. As such, following the behavior of the base method,
195+
/// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
196+
/// are not raised if the current and new value for the target property are the same.
197+
/// </remarks>
198+
protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
199+
{
200+
return SetProperty(oldValue, newValue, EqualityComparer<T>.Default, callback, broadcast, propertyName);
201+
}
202+
203+
/// <summary>
204+
/// Compares the current and new values for a given property. If the value has changed,
205+
/// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
206+
/// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
207+
/// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},bool,string)"/>.
208+
/// </summary>
209+
/// <typeparam name="T">The type of the property that changed.</typeparam>
210+
/// <param name="oldValue">The current property value.</param>
211+
/// <param name="newValue">The property's value after the change occurred.</param>
212+
/// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
213+
/// <param name="callback">A callback to invoke to update the property value.</param>
214+
/// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
215+
/// <param name="propertyName">(optional) The name of the property that changed.</param>
216+
/// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
217+
protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
218+
{
219+
if (!broadcast)
220+
{
221+
return SetProperty(oldValue, newValue, comparer, callback, propertyName);
222+
}
223+
224+
if (SetProperty(oldValue, newValue, comparer, callback, propertyName))
225+
{
226+
Broadcast(oldValue, newValue, propertyName);
227+
228+
return true;
229+
}
230+
231+
return false;
232+
}
233+
}
234+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Threading;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
#nullable enable
10+
11+
namespace Microsoft.Toolkit.Mvvm.DependencyInjection
12+
{
13+
/// <summary>
14+
/// A type that facilitates the use of the <see cref="IServiceProvider"/> type.
15+
/// The <see cref="Ioc"/> provides the ability to configure services in a singleton, thread-safe
16+
/// service provider instance, which can then be used to resolve service instances.
17+
/// The first step to use this feature is to declare some services, for instance:
18+
/// <code>
19+
/// public interface ILogger
20+
/// {
21+
/// void Log(string text);
22+
/// }
23+
/// </code>
24+
/// <code>
25+
/// public class ConsoleLogger : ILogger
26+
/// {
27+
/// void Log(string text) => Console.WriteLine(text);
28+
/// }
29+
/// </code>
30+
/// Then the services configuration should then be done at startup, by calling one of
31+
/// the available <see cref="ConfigureServices(IServiceCollection)"/> overloads, like so:
32+
/// <code>
33+
/// Ioc.Default.ConfigureServices(services =>
34+
/// {
35+
/// services.AddSingleton&lt;ILogger, Logger&gt;();
36+
/// });
37+
/// </code>
38+
/// Finally, you can use the <see cref="Ioc"/> instance (which implements <see cref="IServiceProvider"/>)
39+
/// to retrieve the service instances from anywhere in your application, by doing as follows:
40+
/// <code>
41+
/// Ioc.Default.GetService&lt;ILogger&gt;().Log("Hello world!");
42+
/// </code>
43+
/// </summary>
44+
public sealed class Ioc : IServiceProvider
45+
{
46+
/// <summary>
47+
/// Gets the default <see cref="Ioc"/> instance.
48+
/// </summary>
49+
public static Ioc Default { get; } = new Ioc();
50+
51+
/// <summary>
52+
/// The <see cref="ServiceProvider"/> instance to use, if initialized.
53+
/// </summary>
54+
private ServiceProvider? serviceProvider;
55+
56+
/// <inheritdoc/>
57+
object? IServiceProvider.GetService(Type serviceType)
58+
{
59+
// As per section I.12.6.6 of the official CLI ECMA-335 spec:
60+
// "[...] read and write access to properly aligned memory locations no larger than the native
61+
// word size is atomic when all the write accesses to a location are the same size. Atomic writes
62+
// shall alter no bits other than those written. Unless explicit layout control is used [...],
63+
// data elements no larger than the natural word size [...] shall be properly aligned.
64+
// Object references shall be treated as though they are stored in the native word size."
65+
// The field being accessed here is of native int size (reference type), and is only ever accessed
66+
// directly and atomically by a compare exhange instruction (see below), or here. We can therefore
67+
// assume this read is thread safe with respect to accesses to this property or to invocations to one
68+
// of the available configuration methods. So we can just read the field directly and make the necessary
69+
// check with our local copy, without the need of paying the locking overhead from this get accessor.
70+
ServiceProvider? provider = this.serviceProvider;
71+
72+
if (provider is null)
73+
{
74+
ThrowInvalidOperationExceptionForMissingInitialization();
75+
}
76+
77+
return provider!.GetService(serviceType);
78+
}
79+
80+
/// <summary>
81+
/// Initializes the shared <see cref="IServiceProvider"/> instance.
82+
/// </summary>
83+
/// <param name="setup">The configuration delegate to use to add services.</param>
84+
public void ConfigureServices(Action<IServiceCollection> setup)
85+
{
86+
ConfigureServices(setup, new ServiceProviderOptions());
87+
}
88+
89+
/// <summary>
90+
/// Initializes the shared <see cref="IServiceProvider"/> instance.
91+
/// </summary>
92+
/// <param name="setup">The configuration delegate to use to add services.</param>
93+
/// <param name="options">The <see cref="ServiceProviderOptions"/> instance to configure the service provider behaviors.</param>
94+
public void ConfigureServices(Action<IServiceCollection> setup, ServiceProviderOptions options)
95+
{
96+
var collection = new ServiceCollection();
97+
98+
setup(collection);
99+
100+
ConfigureServices(collection, options);
101+
}
102+
103+
/// <summary>
104+
/// Initializes the shared <see cref="IServiceProvider"/> instance.
105+
/// </summary>
106+
/// <param name="services">The input <see cref="IServiceCollection"/> instance to use.</param>
107+
public void ConfigureServices(IServiceCollection services)
108+
{
109+
ConfigureServices(services, new ServiceProviderOptions());
110+
}
111+
112+
/// <summary>
113+
/// Initializes the shared <see cref="IServiceProvider"/> instance.
114+
/// </summary>
115+
/// <param name="services">The input <see cref="IServiceCollection"/> instance to use.</param>
116+
/// <param name="options">The <see cref="ServiceProviderOptions"/> instance to configure the service provider behaviors.</param>
117+
public void ConfigureServices(IServiceCollection services, ServiceProviderOptions options)
118+
{
119+
ServiceProvider newServices = services.BuildServiceProvider(options);
120+
121+
ServiceProvider? oldServices = Interlocked.CompareExchange(ref this.serviceProvider, newServices, null);
122+
123+
if (!(oldServices is null))
124+
{
125+
ThrowInvalidOperationExceptionForRepeatedConfiguration();
126+
}
127+
}
128+
129+
/// <summary>
130+
/// Throws an <see cref="InvalidOperationException"/> when the <see cref="ServiceProvider"/> property is used before initialization.
131+
/// </summary>
132+
private static void ThrowInvalidOperationExceptionForMissingInitialization()
133+
{
134+
throw new InvalidOperationException("The service provider has not been configured yet");
135+
}
136+
137+
/// <summary>
138+
/// Throws an <see cref="InvalidOperationException"/> when a configuration is attempted more than once.
139+
/// </summary>
140+
private static void ThrowInvalidOperationExceptionForRepeatedConfiguration()
141+
{
142+
throw new InvalidOperationException("The default service provider has already been configured");
143+
}
144+
}
145+
}

0 commit comments

Comments
 (0)