diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index f2c26f710d..3b49e16fc9 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -38,6 +38,11 @@ + + + + + diff --git a/src/ReactiveUI.AndroidX/ControlFetcherMixin.cs b/src/ReactiveUI.AndroidX/ControlFetcherMixin.cs new file mode 100644 index 0000000000..e2522510f9 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ControlFetcherMixin.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Android.Views; + +using static ReactiveUI.ControlFetcherMixin; + +using Fragment = AndroidX.Fragment.App.Fragment; + +namespace ReactiveUI.AndroidX; + +/// +/// ControlFetcherMixin helps you automatically wire-up Activities and +/// Fragments via property names, similar to Butter Knife, as well as allows +/// you to fetch controls manually. +/// +public static class ControlFetcherMixin +{ + /// + /// Wires a control to a property. + /// This should be called in the Fragment's OnCreateView, with the newly inflated layout. + /// + /// The fragment. + /// The inflated view. + /// The resolve members. + public static void WireUpControls(this Fragment fragment, View inflatedView, ResolveStrategy resolveMembers = ResolveStrategy.Implicit) + { + ArgumentNullException.ThrowIfNull(fragment); + + foreach (var member in fragment.GetWireUpMembers(resolveMembers)) + { + try + { + // Find the android control with the same name from the view + var view = inflatedView.GetControl(fragment.GetType().Assembly, member.GetResourceName()); + + // Set the activity field's value to the view with that identifier + member.SetValue(fragment, view); + } + catch (Exception ex) + { + throw new + MissingFieldException("Failed to wire up the Property " + member.Name + " to a View in your layout with a corresponding identifier", ex); + } + } + } +} diff --git a/src/ReactiveUI.AndroidX/GlobalUsings.cs b/src/ReactiveUI.AndroidX/GlobalUsings.cs new file mode 100644 index 0000000000..954a4fb5ce --- /dev/null +++ b/src/ReactiveUI.AndroidX/GlobalUsings.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +global using global::Splat; +global using global::System; +global using global::System.Collections.Generic; +global using global::System.ComponentModel; +global using global::System.Diagnostics.CodeAnalysis; +global using global::System.Linq; +global using global::System.Reactive; +global using global::System.Reactive.Linq; +global using global::System.Reactive.Subjects; +global using global::System.Reactive.Threading.Tasks; +global using global::System.Threading.Tasks; diff --git a/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity.cs b/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity.cs new file mode 100644 index 0000000000..89934d0287 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity.cs @@ -0,0 +1,161 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Android.App; +using Android.Content; +using Android.Runtime; +using AndroidX.AppCompat.App; + +namespace ReactiveUI.AndroidX; + +/// +/// This is an Activity that is both an Activity and has ReactiveObject powers +/// (i.e. you can call RaiseAndSetIfChanged). +/// +public class ReactiveAppCompatActivity : AppCompatActivity, IReactiveObject, IReactiveNotifyPropertyChanged, IHandleObservableErrors +{ + private readonly Subject _activated = new(); + private readonly Subject _deactivated = new(); + private readonly Subject<(int requestCode, Result result, Intent? intent)> _activityResult = new(); + + /// + /// Initializes a new instance of the class. + /// + protected ReactiveAppCompatActivity() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The handle. + /// The ownership. + protected ReactiveAppCompatActivity(in IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + public IObservable> Changing => this.GetChangingObservable(); + + /// + public IObservable> Changed => this.GetChangedObservable(); + + /// + public IObservable ThrownExceptions => this.GetThrownExceptionsObservable(); + + /// + /// Gets a signal when activated. + /// + /// + /// The activated. + /// + public IObservable Activated => _activated.AsObservable(); + + /// + /// Gets a signal when deactivated. + /// + /// + /// The deactivated. + /// + public IObservable Deactivated => _deactivated.AsObservable(); + + /// + /// Gets the activity result. + /// + /// + /// The activity result. + /// + public IObservable<(int requestCode, Result result, Intent? intent)> ActivityResult => _activityResult.AsObservable(); + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + + /// + public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this); + + /// + /// Starts the activity for result asynchronously. + /// + /// The intent. + /// The request code. + /// A task with the result and intent. + public Task<(Result result, Intent? intent)> StartActivityForResultAsync(Intent intent, int requestCode) + { + // NB: It's important that we set up the subscription *before* we + // call ActivityForResult + var ret = ActivityResult + .Where(x => x.requestCode == requestCode) + .Select(x => (x.result, x.intent)) + .FirstAsync() + .ToTask(); + + StartActivityForResult(intent, requestCode); + return ret; + } + + /// + /// Starts the activity for result asynchronously. + /// + /// The type. + /// The request code. + /// A task with the result and intent. + public Task<(Result result, Intent? intent)> StartActivityForResultAsync(Type type, int requestCode) + { + // NB: It's important that we set up the subscription *before* we + // call ActivityForResult + var ret = ActivityResult + .Where(x => x.requestCode == requestCode) + .Select(x => (x.result, x.intent)) + .FirstAsync() + .ToTask(); + + StartActivityForResult(type, requestCode); + return ret; + } + + /// + protected override void OnPause() + { + base.OnPause(); + _deactivated.OnNext(Unit.Default); + } + + /// + protected override void OnResume() + { + base.OnResume(); + _activated.OnNext(Unit.Default); + } + + /// + protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data) + { + base.OnActivityResult(requestCode, resultCode, data); + _activityResult.OnNext((requestCode, resultCode, data)); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _activated.Dispose(); + _deactivated.Dispose(); + _activityResult.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity{TViewModel}.cs b/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity{TViewModel}.cs new file mode 100644 index 0000000000..de69ae77c2 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveAppCompatActivity{TViewModel}.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.AndroidX; + +/// +/// This is an Activity that is both an Activity and has ReactiveObject powers +/// (i.e. you can call RaiseAndSetIfChanged). +/// +/// The view model type. +public class ReactiveAppCompatActivity : ReactiveAppCompatActivity, IViewFor, ICanActivate + where TViewModel : class +{ + private TViewModel? _viewModel; + + /// + /// Initializes a new instance of the class. + /// + protected ReactiveAppCompatActivity() + { + } + + /// + public TViewModel? ViewModel + { + get => _viewModel; + set => this.RaiseAndSetIfChanged(ref _viewModel, value); + } + + /// + object? IViewFor.ViewModel + { + get => _viewModel; + set => _viewModel = (TViewModel?)value; + } +} diff --git a/src/ReactiveUI.AndroidX/ReactiveDialogFragment.cs b/src/ReactiveUI.AndroidX/ReactiveDialogFragment.cs new file mode 100644 index 0000000000..3fa86f7f55 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveDialogFragment.cs @@ -0,0 +1,83 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.AndroidX; + +/// +/// This is a Fragment that is both an Activity and has ReactiveObject powers +/// (i.e. you can call RaiseAndSetIfChanged). +/// +public class ReactiveDialogFragment : global::AndroidX.Fragment.App.DialogFragment, IReactiveNotifyPropertyChanged, IReactiveObject, IHandleObservableErrors +{ + private readonly Subject _activated = new(); + private readonly Subject _deactivated = new(); + + /// + /// Initializes a new instance of the class. + /// + protected ReactiveDialogFragment() + { + } + + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + public IObservable ThrownExceptions => this.GetThrownExceptionsObservable(); + + /// + /// Gets a observable that signals when the fragment is activated. + /// + public IObservable Activated => _activated.AsObservable(); + + /// + /// Gets a observable that signals when the fragment is deactivated. + /// + public IObservable Deactivated => _deactivated.AsObservable(); + + /// + public IObservable> Changing => this.GetChangingObservable(); + + /// + public IObservable> Changed => this.GetChangedObservable(); + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + + /// + public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this); + + /// + public override void OnPause() + { + base.OnPause(); + _deactivated.OnNext(Unit.Default); + } + + /// + public override void OnResume() + { + base.OnResume(); + _activated.OnNext(Unit.Default); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _activated.Dispose(); + _deactivated.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/src/ReactiveUI.AndroidX/ReactiveDialogFragment{TViewModel}.cs b/src/ReactiveUI.AndroidX/ReactiveDialogFragment{TViewModel}.cs new file mode 100644 index 0000000000..97e12f2fe2 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveDialogFragment{TViewModel}.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.AndroidX; + +/// +/// This is a DialogFragment that is both a DialogFragment and has ReactiveObject powers +/// (i.e. you can call RaiseAndSetIfChanged). +/// +/// The view model type. +public class ReactiveDialogFragment : ReactiveDialogFragment, IViewFor, ICanActivate + where TViewModel : class +{ + private TViewModel? _viewModel; + + /// + /// Initializes a new instance of the class. + /// + protected ReactiveDialogFragment() + { + } + + /// + public TViewModel? ViewModel + { + get => _viewModel; + set => this.RaiseAndSetIfChanged(ref _viewModel, value); + } + + /// + object? IViewFor.ViewModel + { + get => _viewModel; + set => _viewModel = (TViewModel?)value; + } +} diff --git a/src/ReactiveUI.AndroidX/ReactiveFragment.cs b/src/ReactiveUI.AndroidX/ReactiveFragment.cs new file mode 100644 index 0000000000..3c694e5e38 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveFragment.cs @@ -0,0 +1,83 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.AndroidX; + +/// +/// This is a Fragment that is both an Activity and has ReactiveObject powers +/// (i.e. you can call RaiseAndSetIfChanged). +/// +public class ReactiveFragment : global::AndroidX.Fragment.App.Fragment, IReactiveNotifyPropertyChanged, IReactiveObject, IHandleObservableErrors +{ + private readonly Subject _activated = new(); + private readonly Subject _deactivated = new(); + + /// + /// Initializes a new instance of the class. + /// + protected ReactiveFragment() + { + } + + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + public IObservable ThrownExceptions => this.GetThrownExceptionsObservable(); + + /// + /// Gets a signal when the fragment is activated. + /// + public IObservable Activated => _activated.AsObservable(); + + /// + /// Gets a signal when the fragment is deactivated. + /// + public IObservable Deactivated => _deactivated.AsObservable(); + + /// + public IObservable> Changing => this.GetChangingObservable(); + + /// + public IObservable> Changed => this.GetChangedObservable(); + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + + /// + public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this); + + /// + public override void OnPause() + { + base.OnPause(); + _deactivated.OnNext(Unit.Default); + } + + /// + public override void OnResume() + { + base.OnResume(); + _activated.OnNext(Unit.Default); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _activated.Dispose(); + _deactivated.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/src/ReactiveUI.AndroidX/ReactiveFragmentActivity.cs b/src/ReactiveUI.AndroidX/ReactiveFragmentActivity.cs new file mode 100644 index 0000000000..9cc7050fa0 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveFragmentActivity.cs @@ -0,0 +1,137 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Android.App; +using Android.Content; + +using AndroidX.Fragment.App; + +namespace ReactiveUI.AndroidX; + +/// +/// This is an Activity that is both an Activity and has ReactiveObject powers +/// (i.e. you can call RaiseAndSetIfChanged). +/// +public class ReactiveFragmentActivity : FragmentActivity, IReactiveObject, IReactiveNotifyPropertyChanged, IHandleObservableErrors +{ + private readonly Subject _activated = new(); + private readonly Subject _deactivated = new(); + private readonly Subject<(int requestCode, Result result, Intent intent)> _activityResult = new(); + + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + public IObservable> Changing => this.GetChangingObservable(); + + /// + public IObservable> Changed => this.GetChangedObservable(); + + /// + public IObservable ThrownExceptions => this.GetThrownExceptionsObservable(); + + /// + /// Gets a signal when the activity fragment is activated. + /// + public IObservable Activated => _activated.AsObservable(); + + /// + /// Gets a signal when the activity fragment is deactivated. + /// + public IObservable Deactivated => _deactivated.AsObservable(); + + /// + /// Gets the activity result. + /// + public IObservable<(int requestCode, Result result, Intent intent)> ActivityResult => _activityResult.AsObservable(); + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + + /// + public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this); + + /// + /// Starts the activity for result asynchronously. + /// + /// The intent. + /// The request code. + /// A task with the result and intent. + public Task<(Result result, Intent intent)> StartActivityForResultAsync(Intent intent, int requestCode) + { + // NB: It's important that we set up the subscription *before* we + // call ActivityForResult + var ret = ActivityResult + .Where(x => x.requestCode == requestCode) + .Select(x => (x.result, x.intent)) + .FirstAsync() + .ToTask(); + + StartActivityForResult(intent, requestCode); + return ret; + } + + /// + /// Starts the activity for result asynchronously. + /// + /// The type. + /// The request code. + /// A task with the result and intent. + public Task<(Result result, Intent intent)> StartActivityForResultAsync(Type type, int requestCode) + { + // NB: It's important that we set up the subscription *before* we + // call ActivityForResult + var ret = ActivityResult + .Where(x => x.requestCode == requestCode) + .Select(x => (x.result, x.intent)) + .FirstAsync() + .ToTask(); + + StartActivityForResult(type, requestCode); + return ret; + } + + /// + protected override void OnPause() + { + base.OnPause(); + _deactivated.OnNext(Unit.Default); + } + + /// + protected override void OnResume() + { + base.OnResume(); + _activated.OnNext(Unit.Default); + } + + /// + protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data) + { + ArgumentNullException.ThrowIfNull(data); + + base.OnActivityResult(requestCode, resultCode, data); + _activityResult.OnNext((requestCode, resultCode, data)); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _activated.Dispose(); + _deactivated.Dispose(); + _activityResult.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/src/ReactiveUI.AndroidX/ReactiveFragmentActivity{TViewModel}.cs b/src/ReactiveUI.AndroidX/ReactiveFragmentActivity{TViewModel}.cs new file mode 100644 index 0000000000..b60743ae8d --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveFragmentActivity{TViewModel}.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.AndroidX; + +/// +/// This is an Activity that is both an Activity and has ReactiveObject powers +/// (i.e. you can call RaiseAndSetIfChanged). +/// +/// The view model type. +public class ReactiveFragmentActivity : ReactiveFragmentActivity, IViewFor, ICanActivate + where TViewModel : class +{ + private TViewModel? _viewModel; + + /// + /// Initializes a new instance of the class. + /// + protected ReactiveFragmentActivity() + { + } + + /// + public TViewModel? ViewModel + { + get => _viewModel; + set => this.RaiseAndSetIfChanged(ref _viewModel, value); + } + + /// + object? IViewFor.ViewModel + { + get => _viewModel; + set => _viewModel = (TViewModel?)value; + } +} diff --git a/src/ReactiveUI.AndroidX/ReactiveFragment{TViewModel}.cs b/src/ReactiveUI.AndroidX/ReactiveFragment{TViewModel}.cs new file mode 100644 index 0000000000..1faa368726 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveFragment{TViewModel}.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.AndroidX; + +/// +/// This is a Fragment that is both an Activity and has ReactiveObject powers +/// (i.e. you can call RaiseAndSetIfChanged). +/// +/// The view model type. +public class ReactiveFragment : ReactiveFragment, IViewFor, ICanActivate + where TViewModel : class +{ + private TViewModel? _viewModel; + + /// + /// Initializes a new instance of the class. + /// + protected ReactiveFragment() + { + } + + /// + public TViewModel? ViewModel + { + get => _viewModel; + set => this.RaiseAndSetIfChanged(ref _viewModel, value); + } + + /// + object? IViewFor.ViewModel + { + get => _viewModel; + set => _viewModel = (TViewModel?)value; + } +} diff --git a/src/ReactiveUI.AndroidX/ReactivePagerAdapter.cs b/src/ReactiveUI.AndroidX/ReactivePagerAdapter.cs new file mode 100644 index 0000000000..76fb1f8487 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactivePagerAdapter.cs @@ -0,0 +1,100 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Android.Views; + +using AndroidX.ViewPager.Widget; + +using DynamicData; + +using Object = Java.Lang.Object; + +namespace ReactiveUI.AndroidX; + +/// +/// ReactivePagerAdapter is a PagerAdapter that will interface with a +/// Observable change set, in a similar fashion to ReactiveTableViewSource. +/// +/// The view model type. +public class ReactivePagerAdapter : PagerAdapter, IEnableLogger + where TViewModel : class +{ + private readonly SourceList _list; + private readonly Func _viewCreator; + private readonly Action? _viewInitializer; + private readonly IDisposable _inner; + + /// + /// Initializes a new instance of the class. + /// + /// The change set to page. + /// A function which will create the view. + /// A action which will initialize a view. + public ReactivePagerAdapter( + IObservable> changeSet, + Func viewCreator, + Action? viewInitializer = null) + { + _list = new SourceList(changeSet); + _viewCreator = viewCreator; + _viewInitializer = viewInitializer; + + _inner = _list.Connect().Subscribe(_ => NotifyDataSetChanged()); + } + + /// + public override int Count => _list.Count; + + /// + public override bool IsViewFromObject(View view, Object @object) => (View)@object == view; + + /// + public override Object InstantiateItem(ViewGroup container, int position) + { + ArgumentNullException.ThrowIfNull(container); + + var data = _list.Items[position]; + + // NB: PagerAdapter does not recycle itself. + var theView = _viewCreator(data, container); + + if (theView.GetViewHost() is IViewFor ivf) + { + ivf.ViewModel = data; + } + + _viewInitializer?.Invoke(data, theView); + + container.AddView(theView, 0); + return theView; + } + + /// + public override void DestroyItem(ViewGroup container, int position, Object @object) + { + ArgumentNullException.ThrowIfNull(container); + + ArgumentNullException.ThrowIfNull(@object); + + if (@object is not View view) + { + throw new ArgumentException("Item must be of type View", nameof(@object)); + } + + container.RemoveView(view); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _inner.Dispose(); + _list.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/src/ReactiveUI.AndroidX/ReactivePagerAdapter{TViewModel,TCollection}.cs b/src/ReactiveUI.AndroidX/ReactivePagerAdapter{TViewModel,TCollection}.cs new file mode 100644 index 0000000000..5af87b1b36 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactivePagerAdapter{TViewModel,TCollection}.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Collections.Specialized; + +using Android.Views; + +using DynamicData; +using DynamicData.Binding; + +namespace ReactiveUI.AndroidX; + +/// +/// ReactivePagerAdapter is a PagerAdapter that will interface with a +/// Observable change set, in a similar fashion to ReactiveTableViewSource. +/// +/// The view model type. +/// The type of collection. +/// +/// Initializes a new instance of the class. +/// +/// The collection to page. +/// The function which will create the view. +/// A action which will initialize the view. +public class ReactivePagerAdapter( + TCollection collection, + Func viewCreator, + Action? viewInitializer = null) : ReactivePagerAdapter(collection.ToObservableChangeSet(), viewCreator, viewInitializer) + where TViewModel : class + where TCollection : INotifyCollectionChanged, IEnumerable; diff --git a/src/ReactiveUI.AndroidX/ReactivePreferenceFragment.cs b/src/ReactiveUI.AndroidX/ReactivePreferenceFragment.cs new file mode 100644 index 0000000000..93ea575e4b --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactivePreferenceFragment.cs @@ -0,0 +1,96 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Android.Runtime; +using AndroidX.Preference; + +namespace ReactiveUI.AndroidX; + +/// +/// This is a PreferenceFragment that is both an Activity and has ReactiveObject powers +/// (i.e. you can call RaiseAndSetIfChanged). +/// +public abstract class ReactivePreferenceFragment : PreferenceFragmentCompat, IReactiveNotifyPropertyChanged, IReactiveObject, IHandleObservableErrors +{ + private readonly Subject _activated = new(); + private readonly Subject _deactivated = new(); + + /// + /// Initializes a new instance of the class. + /// + protected ReactivePreferenceFragment() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The handle. + /// The ownership. + protected ReactivePreferenceFragment(in IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + public IObservable> Changing => this.GetChangingObservable(); + + /// + public IObservable> Changed => this.GetChangedObservable(); + + /// + public IObservable ThrownExceptions => this.GetThrownExceptionsObservable(); + + /// + /// Gets a signal when the fragment is activated. + /// + public IObservable Activated => _activated.AsObservable(); + + /// + /// Gets a signal when the fragment is deactivated. + /// + public IObservable Deactivated => _deactivated.AsObservable(); + + /// + public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + + /// + public override void OnPause() + { + base.OnPause(); + _deactivated.OnNext(Unit.Default); + } + + /// + public override void OnResume() + { + base.OnResume(); + _activated.OnNext(Unit.Default); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _activated.Dispose(); + _deactivated.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/src/ReactiveUI.AndroidX/ReactivePreferenceFragment{TViewModel}.cs b/src/ReactiveUI.AndroidX/ReactivePreferenceFragment{TViewModel}.cs new file mode 100644 index 0000000000..bb31a71e87 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactivePreferenceFragment{TViewModel}.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Android.Runtime; + +namespace ReactiveUI.AndroidX; + +/// +/// This is a PreferenceFragment that is both an Activity and has ReactiveObject powers +/// (i.e. you can call RaiseAndSetIfChanged). +/// +/// The view model type. +public abstract class ReactivePreferenceFragment : ReactivePreferenceFragment, IViewFor, ICanActivate + where TViewModel : class +{ + private TViewModel? _viewModel; + + /// + /// Initializes a new instance of the class. + /// + protected ReactivePreferenceFragment() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The handle. + /// The ownership. + protected ReactivePreferenceFragment(in IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + /// + public TViewModel? ViewModel + { + get => _viewModel; + set => this.RaiseAndSetIfChanged(ref _viewModel, value); + } + + /// + object? IViewFor.ViewModel + { + get => _viewModel; + set => _viewModel = (TViewModel?)value; + } +} diff --git a/src/ReactiveUI.AndroidX/ReactiveRecyclerViewAdapter.cs b/src/ReactiveUI.AndroidX/ReactiveRecyclerViewAdapter.cs new file mode 100644 index 0000000000..3e2d212f65 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveRecyclerViewAdapter.cs @@ -0,0 +1,104 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using AndroidX.RecyclerView.Widget; +using DynamicData; + +namespace ReactiveUI.AndroidX; + +/// +/// An adapter for the Android . +/// +/// The type of ViewModel that this adapter holds. +public abstract class ReactiveRecyclerViewAdapter : RecyclerView.Adapter + where TViewModel : class, IReactiveObject +{ + private readonly SourceList _list; + + private readonly IDisposable _inner; + + /// + /// Initializes a new instance of the class. + /// + /// The backing list. + protected ReactiveRecyclerViewAdapter(IObservable> backingList) + { + _list = new SourceList(backingList); + + _inner = _list + .Connect() + .ForEachChange(UpdateBindings) + .Subscribe(); + } + + /// + public override int ItemCount => _list.Count; + + /// + public override int GetItemViewType(int position) => GetItemViewType(position, GetViewModelByPosition(position)); + + /// + /// Determine the View that will be used/re-used in lists where + /// the list contains different cell designs. + /// + /// The position of the current view in the list. + /// The ViewModel associated with the current View. + /// An ID to be used in OnCreateViewHolder. + public virtual int GetItemViewType(int position, TViewModel? viewModel) => base.GetItemViewType(position); + + /// + public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position) + { + ArgumentNullException.ThrowIfNull(holder); + + if (holder is not IViewFor viewForHolder) + { + throw new ArgumentException("Holder must be derived from IViewFor", nameof(holder)); + } + + viewForHolder.ViewModel = GetViewModelByPosition(position); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _inner.Dispose(); + _list.Dispose(); + } + + base.Dispose(disposing); + } + + private TViewModel? GetViewModelByPosition(int position) => position >= _list.Count ? null : _list.Items[position]; + + private void UpdateBindings(Change change) + { + switch (change.Reason) + { + case ListChangeReason.Add: + NotifyItemInserted(change.Item.CurrentIndex); + break; + case ListChangeReason.Remove: + NotifyItemRemoved(change.Item.CurrentIndex); + break; + case ListChangeReason.Moved: + NotifyItemMoved(change.Item.PreviousIndex, change.Item.CurrentIndex); + break; + case ListChangeReason.Replace: + case ListChangeReason.Refresh: + NotifyItemChanged(change.Item.CurrentIndex); + break; + case ListChangeReason.AddRange: + NotifyItemRangeInserted(change.Range.Index, change.Range.Count); + break; + case ListChangeReason.RemoveRange: + case ListChangeReason.Clear: + NotifyItemRangeRemoved(change.Range.Index, change.Range.Count); + break; + } + } +} diff --git a/src/ReactiveUI.AndroidX/ReactiveRecyclerViewAdapter{TViewModel,TCollection}.cs b/src/ReactiveUI.AndroidX/ReactiveRecyclerViewAdapter{TViewModel,TCollection}.cs new file mode 100644 index 0000000000..1059bde3db --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveRecyclerViewAdapter{TViewModel,TCollection}.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Collections.Specialized; + +using AndroidX.RecyclerView.Widget; + +using DynamicData; +using DynamicData.Binding; + +namespace ReactiveUI.AndroidX; + +/// +/// An adapter for the Android . +/// +/// The type of ViewModel that this adapter holds. +/// The type of collection. +/// +/// Initializes a new instance of the class. +/// +/// The backing list. +public abstract class ReactiveRecyclerViewAdapter(TCollection backingList) : ReactiveRecyclerViewAdapter(backingList.ToObservableChangeSet()) + where TViewModel : class, IReactiveObject + where TCollection : ICollection, INotifyCollectionChanged; diff --git a/src/ReactiveUI.AndroidX/ReactiveRecyclerViewViewHolder.cs b/src/ReactiveUI.AndroidX/ReactiveRecyclerViewViewHolder.cs new file mode 100644 index 0000000000..89aa191cc6 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveRecyclerViewViewHolder.cs @@ -0,0 +1,204 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Reflection; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Android.Views; +using AndroidX.RecyclerView.Widget; + +namespace ReactiveUI.AndroidX; + +/// +/// A implementation that binds to a reactive view model. +/// +/// The type of the view model. +public class ReactiveRecyclerViewViewHolder : RecyclerView.ViewHolder, ILayoutViewHost, IViewFor, IReactiveNotifyPropertyChanged>, IReactiveObject, ICanActivate + where TViewModel : class, IReactiveObject +{ + /// + /// Gets all public accessible properties. + /// + [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401: Field should be private", Justification = "Legacy reasons")] + [SuppressMessage("Design", "CA1051: Do not declare visible instance fields", Justification = "Legacy reasons")] + [IgnoreDataMember] + [JsonIgnore] + protected Lazy? AllPublicProperties; + + private readonly Subject _activated = new(); + + private readonly Subject _deactivated = new(); + + private TViewModel? _viewModel; + + /// + /// Initializes a new instance of the class. + /// + /// The view. + protected ReactiveRecyclerViewViewHolder(View view) + : base(view) + { + SetupRxObj(); + ArgumentNullException.ThrowIfNull(view); + + view.ViewAttachedToWindow += OnViewAttachedToWindow; + view.ViewDetachedFromWindow += OnViewDetachedFromWindow; + + Selected = Observable.FromEvent( + eventHandler => + { + void Handler(object? sender, EventArgs e) => eventHandler(AbsoluteAdapterPosition); + return Handler; + }, + h => view.Click += h, + h => view.Click -= h); + + LongClicked = Observable.FromEvent, int>( + eventHandler => + { + void Handler(object? sender, View.LongClickEventArgs e) => eventHandler(AbsoluteAdapterPosition); + + return Handler; + }, + h => view.LongClick += h, + h => view.LongClick -= h); + + SelectedWithViewModel = Observable.FromEvent( + eventHandler => + { + void Handler(object? sender, EventArgs e) => eventHandler(ViewModel); + return Handler; + }, + h => view.Click += h, + h => view.Click -= h); + + LongClickedWithViewModel = Observable.FromEvent, TViewModel?>( + eventHandler => + { + void Handler(object? sender, View.LongClickEventArgs e) => eventHandler(ViewModel); + return Handler; + }, + h => view.LongClick += h, + h => view.LongClick -= h); + } + + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Gets an observable that signals that this ViewHolder has been selected. + /// + /// The is the position of this ViewHolder in the + /// and corresponds to the property. + /// + /// + public IObservable Selected { get; } + + /// + /// Gets an observable that signals that this ViewHolder has been selected. + /// The is the ViewModel of this ViewHolder in the . + /// + public IObservable SelectedWithViewModel { get; } + + /// + /// Gets an observable that signals that this ViewHolder has been long-clicked. + /// + /// The is the position of this ViewHolder in the + /// and corresponds to the property. + /// + /// + public IObservable LongClicked { get; } + + /// + /// Gets an observable that signals that this ViewHolder has been long-clicked. + /// The is the ViewModel of this ViewHolder in the . + /// + public IObservable LongClickedWithViewModel { get; } + + /// + public IObservable Activated => _activated.AsObservable(); + + /// + public IObservable Deactivated => _deactivated.AsObservable(); + + /// + /// Gets the current view being shown. + /// + public View View => ItemView; + + /// + public TViewModel? ViewModel + { + get => _viewModel; + set => this.RaiseAndSetIfChanged(ref _viewModel, value); + } + + /// + /// Gets an observable which signals when exceptions are thrown. + /// + [IgnoreDataMember] + [JsonIgnore] + public IObservable ThrownExceptions => this.GetThrownExceptionsObservable(); + + /// + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (TViewModel?)value; + } + + /// + [IgnoreDataMember] + [JsonIgnore] + public IObservable>> Changing => this.GetChangingObservable(); + + /// + [IgnoreDataMember] + [JsonIgnore] + public IObservable>> Changed => this.GetChangedObservable(); + + /// + public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this); + + /// + /// Gets if change notifications via the INotifyPropertyChanged interface are being sent. + /// + /// A value indicating whether change notifications are enabled or not. + public bool AreChangeNotificationsEnabled() => IReactiveObjectExtensions.AreChangeNotificationsEnabled(this); + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + View.ViewAttachedToWindow -= OnViewAttachedToWindow; + View.ViewDetachedFromWindow -= OnViewDetachedFromWindow; + + _activated.Dispose(); + _deactivated.Dispose(); + } + + base.Dispose(disposing); + } + + [OnDeserialized] + private void SetupRxObj(in StreamingContext sc) => SetupRxObj(); + + private void SetupRxObj() => + AllPublicProperties = new Lazy(() => [.. GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)]); + + private void OnViewAttachedToWindow(object? sender, View.ViewAttachedToWindowEventArgs args) => _activated.OnNext(Unit.Default); + + private void OnViewDetachedFromWindow(object? sender, View.ViewDetachedFromWindowEventArgs args) => _deactivated.OnNext(Unit.Default); +} diff --git a/src/ReactiveUI.AndroidX/ReactiveUI.AndroidX.csproj b/src/ReactiveUI.AndroidX/ReactiveUI.AndroidX.csproj new file mode 100644 index 0000000000..553336a9d8 --- /dev/null +++ b/src/ReactiveUI.AndroidX/ReactiveUI.AndroidX.csproj @@ -0,0 +1,19 @@ + + + net8.0-android + Provides ReactiveUI extensions for the AndroidX Library + ReactiveUI.AndroidX + mvvm;reactiveui;rx;reactive extensions;observable;LINQ;events;frp;xamarin;androidx;forms;xamarin.androidx;net; + 34.0 + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet6_0.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet6_0.verified.txt index ca4c6ffa6f..a9f915f06b 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet6_0.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet6_0.verified.txt @@ -1,4 +1,5 @@ -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Blazor")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.AndroidX")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Blazor")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Drawing")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Maui")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Tests")] diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt index 1910660e09..bb602deebe 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt @@ -1,4 +1,5 @@ -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Blazor")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.AndroidX")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Blazor")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Drawing")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Maui")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Tests")] diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt index a94975353f..781f7821ce 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt @@ -1,4 +1,5 @@ -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Blazor")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.AndroidX")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Blazor")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Drawing")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Maui")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ReactiveUI.Tests")] diff --git a/src/ReactiveUI.sln b/src/ReactiveUI.sln index b6484b67a4..e60b06e689 100644 --- a/src/ReactiveUI.sln +++ b/src/ReactiveUI.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 +# 17 VisualStudioVersion = 17.0.31825.309 MinimumVisualStudioVersion = 16.0.31613.86 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BD9762CF-E104-481C-96A6-26E624B86283}" @@ -47,6 +47,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Maui", "Reactive EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.WinUI", "ReactiveUI.WinUI\ReactiveUI.WinUI.csproj", "{4FF9F04B-928E-47B6-836F-546B584F597C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.AndroidX", "ReactiveUI.AndroidX\ReactiveUI.AndroidX.csproj", "{3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -261,6 +263,22 @@ Global {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|x64.Build.0 = Release|Any CPU {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|x86.ActiveCfg = Release|Any CPU {4FF9F04B-928E-47B6-836F-546B584F597C}.Release|x86.Build.0 = Release|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|arm64.ActiveCfg = Debug|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|arm64.Build.0 = Debug|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|x64.ActiveCfg = Debug|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|x64.Build.0 = Debug|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|x86.ActiveCfg = Debug|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Debug|x86.Build.0 = Debug|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|Any CPU.Build.0 = Release|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|arm64.ActiveCfg = Release|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|arm64.Build.0 = Release|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x64.ActiveCfg = Release|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x64.Build.0 = Release|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x86.ActiveCfg = Release|Any CPU + {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ReactiveUI/Properties/AssemblyInfo.cs b/src/ReactiveUI/Properties/AssemblyInfo.cs index 21cb45aab7..ef738da8ff 100644 --- a/src/ReactiveUI/Properties/AssemblyInfo.cs +++ b/src/ReactiveUI/Properties/AssemblyInfo.cs @@ -14,3 +14,4 @@ [assembly: InternalsVisibleTo("ReactiveUI.Uno.WinUI")] [assembly: InternalsVisibleTo("ReactiveUI.Drawing")] [assembly: InternalsVisibleTo("ReactiveUI.WinUI")] +[assembly: InternalsVisibleTo("ReactiveUI.AndroidX")]