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

fix: VS2019 android wire up controls throws exception #2050

@@ -8,12 +8,9 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using Android.App;
using Android.Support.V7.App;
using Android.Views;
using Java.Interop;
using static ReactiveUI.ControlFetcherMixin;

namespace ReactiveUI.AndroidSupport
{
@@ -24,122 +21,182 @@ namespace ReactiveUI.AndroidSupport
/// </summary>
public static class ControlFetcherMixin
{
private static readonly Dictionary<string, int> controlIds;

private static readonly ConditionalWeakTable<object, Dictionary<string, View>> viewCache =
new ConditionalWeakTable<object, Dictionary<string, View>>();

private static readonly MethodInfo getControlActivity;
private static readonly MethodInfo getControlView;

static ControlFetcherMixin()
{
// NB: This is some hacky shit, but on Xamarin.Android at the moment,
// this is always the entry assembly.
var assm = AppDomain.CurrentDomain.GetAssemblies()[1];
var resources = assm.GetModules().SelectMany(x => x.GetTypes()).First(x => x.Name == "Resource");

controlIds = resources.GetNestedType("Id").GetFields()
.Where(x => x.FieldType == typeof(int))
.ToDictionary(k => k.Name.ToLowerInvariant(), v => (int)v.GetRawConstantValue());

var type = typeof(ControlFetcherMixin);
getControlActivity = type.GetMethod("GetControl", new[] { typeof(AppCompatActivity), typeof(string) });
getControlView = type.GetMethod("GetControl", new[] { typeof(View), typeof(string) });
}

/// <summary>
/// Gets the control from an activiy.
/// Gets the control from an activity.
/// </summary>
/// <typeparam name="T">The control type.</typeparam>
/// <param name="this">The activity.</param>
/// <param name="activity">The activity.</param>
/// <param name="propertyName">The property name.</param>
/// <returns>Returns a view.</returns>
public static T GetControl<T>(this AppCompatActivity @this, [CallerMemberName]string propertyName = null)
where T : View => (T)GetCachedControl(propertyName, @this, () => @this.FindViewById(controlIds[propertyName.ToLowerInvariant()]).JavaCast<T>());
/// <returns>The return view.</returns>
public static T GetControl<T>(this Activity activity, [CallerMemberName] string propertyName = null)
where T : View => (T)GetCachedControl(propertyName, activity, () => activity
.FindViewById(GetResourceId(activity, propertyName))
.JavaCast<T>());

/// <summary>
/// Gets the control from an activity.
/// </summary>
/// <typeparam name="T">The control type.</typeparam>
/// <param name="view">The view.</param>
/// <param name="propertyName">The property.</param>
/// <returns>The return view.</returns>
public static T GetControl<T>(this View view, [CallerMemberName] string propertyName = null)
where T : View => (T)GetCachedControl(propertyName, view, () => view
.FindViewById(GetResourceId(view, propertyName))
.JavaCast<T>());

/// <summary>
/// Gets the control from a view.
/// Gets the control from an activity.
/// </summary>
/// <typeparam name="T">The control type.</typeparam>
/// <param name="this">The view.</param>
/// <param name="fragment">The fragment.</param>
/// <param name="propertyName">The property name.</param>
/// <returns>A <see cref="View"/>.</returns>
public static T GetControl<T>(this View @this, [CallerMemberName]string propertyName = null)
where T : View => (T)GetCachedControl(propertyName, @this, () => @this.FindViewById(controlIds[propertyName.ToLowerInvariant()]).JavaCast<T>());
/// <returns>The return view.</returns>
public static T GetControl<T>(this Fragment fragment, [CallerMemberName] string propertyName = null)
where T : View => GetControl<T>(fragment.View, propertyName);

/// <summary>
/// Wires a control to a property.
/// </summary>
/// <param name="layoutHost">The layout view host.</param>
/// <param name="resolveMembers">The resolve members.</param>
public static void WireUpControls(this ILayoutViewHost layoutHost, ReactiveUI.ControlFetcherMixin.ResolveStrategy resolveMembers = ReactiveUI.ControlFetcherMixin.ResolveStrategy.Implicit)
{
var members = layoutHost.GetWireUpMembers(resolveMembers).ToList();
members.ForEach(m =>
{
try
{
var view = layoutHost.View.GetControlInternal(m.GetResourceName());
m.SetValue(layoutHost, view);
}
catch (Exception ex)
{
throw new
MissingFieldException("Failed to wire up the Property " + m.Name + " to a View in your layout with a corresponding identifier", ex);
}
});
}

/// <summary>
/// A helper method to automatically resolve properties in an <see cref="Android.Support.V4.App.Fragment"/> to their respective elements in the layout.
/// This should be called in the Fragement's OnCreateView, with the newly inflated layout.
/// Wires a control to a property.
/// </summary>
/// <param name="this">The fragment.</param>
/// <param name="inflatedView">The newly inflated <see cref="View"/> returned from Inflate.</param>
/// <param name="resolveMembers">The strategy used to resolve properties that either subclass <see cref="View"/>, have a <see cref="WireUpResourceAttribute"/> or have a <see cref="IgnoreResourceAttribute"/>.</param>
public static void WireUpControls(this Android.Support.V4.App.Fragment @this, View inflatedView, ResolveStrategy resolveMembers = ResolveStrategy.Implicit)
/// <param name="view">The view.</param>
/// <param name="resolveMembers">The resolve members.</param>
public static void WireUpControls(this View view, ReactiveUI.ControlFetcherMixin.ResolveStrategy resolveMembers = ReactiveUI.ControlFetcherMixin.ResolveStrategy.Implicit)
{
var members = @this.GetWireUpMembers(resolveMembers);
var members = view.GetWireUpMembers(resolveMembers);

members.ToList().ForEach(m =>
This conversation was marked as resolved by glennawatson

This comment has been minimized.

Copy link
@glennawatson

glennawatson May 25, 2019

Contributor

Given you have to do ToList() I think you should just do a normal regular foreach (var member in members) -- avoids the extra allocations.

{
try
{
// Find the android control with the same name
var currentView = view.GetControlInternal(m.GetResourceName());

// Set the activity field's value to the view with that identifier
m.SetValue(view, currentView);
}
catch (Exception ex)
{
throw new
MissingFieldException("Failed to wire up the Property " + m.Name + " to a View in your layout with a corresponding identifier", ex);
}
});
}

/// <summary>
/// Wires a control to a property.
/// This should be called in the Fragment's OnCreateView, with the newly inflated layout.
/// </summary>
/// <param name="fragment">The fragment.</param>
/// <param name="inflatedView">The inflated view.</param>
/// <param name="resolveMembers">The resolve members.</param>
public static void WireUpControls(this Fragment fragment, View inflatedView, ReactiveUI.ControlFetcherMixin.ResolveStrategy resolveMembers =
ReactiveUI.ControlFetcherMixin.ResolveStrategy.Implicit)
{
var members = fragment.GetWireUpMembers(resolveMembers);

members.ToList().ForEach(m =>
{
try
{
// Find the android control with the same name from the view
var view = inflatedView.GetControlInternal(m.PropertyType, m.GetResourceName());
var view = inflatedView.GetControlInternal(m.GetResourceName());

// Set the activity field's value to the view with that identifier
m.SetValue(@this, view);
m.SetValue(fragment, view);
}
catch (Exception ex)
{
throw new MissingFieldException(
"Failed to wire up the Property "
+ m.Name + " to a View in your layout with a corresponding identifier", ex);
throw new
MissingFieldException("Failed to wire up the Property " + m.Name + " to a View in your layout with a corresponding identifier", ex);
}
});
}

// Copied from ReactiveUI/Platforms/android/ControlFetcherMixins.cs
private static IEnumerable<PropertyInfo> GetWireUpMembers(this object @this, ResolveStrategy resolveStrategy)
/// <summary>
/// Wires a control to a property.
/// </summary>
/// <param name="activity">The Activity.</param>
/// <param name="resolveMembers">The resolve members.</param>
public static void WireUpControls(this Activity activity, ReactiveUI.ControlFetcherMixin.ResolveStrategy resolveMembers = ReactiveUI.ControlFetcherMixin.ResolveStrategy.Implicit)
{
var members = @this.GetType().GetRuntimeProperties();
var members = activity.GetWireUpMembers(resolveMembers);

switch (resolveStrategy)
members.ToList().ForEach(m =>
This conversation was marked as resolved by glennawatson

This comment has been minimized.

Copy link
@glennawatson

glennawatson May 25, 2019

Contributor

same here, just use the regular list, and avoid the list allocation.

{
default: // Implicit uses the default case.
return members.Where(m => m.PropertyType.IsSubclassOf(typeof(View))
|| m.GetCustomAttribute<WireUpResourceAttribute>(true) != null);
try
{
// Find the android control with the same name
var view = activity.GetControlInternal(m.GetResourceName());

case ResolveStrategy.ExplicitOptIn:
return members.Where(m => m.GetCustomAttribute<WireUpResourceAttribute>(true) != null);
// Set the activity field's value to the view with that identifier
m.SetValue(activity, view);
}
catch (Exception ex)
{
throw new
MissingFieldException("Failed to wire up the Property " + m.Name + " to a View in your layout with a corresponding identifier", ex);
}
});
}

case ResolveStrategy.ExplicitOptOut:
return members.Where(m => typeof(View).IsAssignableFrom(m.PropertyType)
&& m.GetCustomAttribute<IgnoreResourceAttribute>(true) == null);
}
private static View GetControlInternal(this View parent, string resourceName)
{
var context = parent.Context;
var res = context.Resources;
var id = res.GetIdentifier(resourceName, "id", context.PackageName);
return parent.FindViewById(id);
}

// Also copied from ReactiveUI/Platforms/android/ControlFetcherMixins.cs
private static string GetResourceName(this PropertyInfo member)
private static View GetControlInternal(this Activity parent, string resourceName)
{
var resourceNameOverride = member.GetCustomAttribute<WireUpResourceAttribute>()?.ResourceNameOverride;
return resourceNameOverride ?? member.Name;
return parent.FindViewById(GetResourceId(parent, resourceName));
}

private static View GetControlInternal(this View parent, Type viewType, string name)
private static int GetResourceId(Activity activity, string resourceName)
{
var mi = getControlView.MakeGenericMethod(new[] { viewType });
return (View)mi.Invoke(null, new object[] { parent, name });
var res = activity.Resources;
return res.GetIdentifier(resourceName, "id", activity.PackageName);
}

private static View GetControlInternal(this AppCompatActivity parent, Type viewType, string name)
private static int GetResourceId(View view, string resourceName)
{
var mi = getControlActivity.MakeGenericMethod(new[] { viewType });
return (View)mi.Invoke(null, new object[] { parent, name });
var res = view.Context.Resources;
return res.GetIdentifier(resourceName, "id", view.Context.PackageName);
}

private static View GetCachedControl(string propertyName, object rootView, Func<View> fetchControlFromView)
{
var ret = default(View);
View ret;
var ourViewCache = viewCache.GetOrCreateValue(rootView);

if (ourViewCache.TryGetValue(propertyName, out ret))
@@ -152,5 +209,31 @@ private static View GetCachedControl(string propertyName, object rootView, Func<
ourViewCache.Add(propertyName, ret);
return ret;
}

private static string GetResourceName(this PropertyInfo member)
{
var resourceNameOverride = member.GetCustomAttribute<WireUpResourceAttribute>()?.ResourceNameOverride;
return resourceNameOverride ?? member.Name;
}

private static IEnumerable<PropertyInfo> GetWireUpMembers(this object @this, ReactiveUI.ControlFetcherMixin.ResolveStrategy
resolveStrategy)
{
var members = @this.GetType().GetRuntimeProperties();

switch (resolveStrategy)
{
default: // Implicit matches the Default.
return members.Where(m => m.PropertyType.IsSubclassOf(typeof(View))
|| m.GetCustomAttribute<WireUpResourceAttribute>(true) != null);

case ReactiveUI.ControlFetcherMixin.ResolveStrategy.ExplicitOptIn:
return members.Where(m => m.GetCustomAttribute<WireUpResourceAttribute>(true) != null);

case ReactiveUI.ControlFetcherMixin.ResolveStrategy.ExplicitOptOut:
return members.Where(m => typeof(View).IsAssignableFrom(m.PropertyType)
&& m.GetCustomAttribute<IgnoreResourceAttribute>(true) == null);
}
}
}
}
}
@@ -4,6 +4,7 @@
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ReactiveUI.Fody.deps" minOccurs="0" maxOccurs="1" type="xs:anyType" />
This conversation was marked as resolved by glennawatson

This comment has been minimized.

Copy link
@glennawatson

glennawatson May 25, 2019

Contributor

Don't think we need this file checked in.

<xs:element name="ReactiveUI" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.