Skip to content

.NET for Android has no built-in way to generate a property for each control in a layout. WireUpControls fills that gap. Call it once, and it finds every eligible property on your Activity, Fragment, View or ILayoutViewHost. It assigns each one the control with the matching resource ID, instead of a FindViewById call per field. It works the way Butterknife does for Java Android code. The school timetable app uses it throughout. The examples below are all from that app.

Naming policy

WireUpControls builds a dictionary of every resource ID name in your layouts, lowercased, mapped to the actual resource ID. The mapping mirrors the generated Resource.designer.cs file. Because the lookup is case-insensitive, you cannot reuse the same resource name with two different casings across your layouts. Android generates a distinct ID for each casing, so there is no single ID WireUpControls could map both to. Layout files commonly use a lower-case-first name such as subjectLabel, while a C# property uses an upper-case-first name such as SubjectLabel. The lowercased lookup is why that difference does not need a [WireUpResource] override.

Resolve strategies

Three ControlFetcherMixins.ResolveStrategy values choose which properties WireUpControls wires.

StrategyWires
ImplicitEvery writable property whose type is a View subclass, or that carries [WireUpResource]
ExplicitOptInOnly properties that carry [WireUpResource]
ExplicitOptOutEvery writable View-typed property except one that carries [IgnoreResource]

Implicit

Implicit is the default: call WireUpControls() with no argument, and it wires every View-typed writable property regardless of visibility. MainActivity's home screen uses it for three properties, all resolved by their like-named resources in activity_main.xml:

this.WireUpControls();

PropertyInfo[] wiredMembers = this.GetWireUpMembers(ControlFetcherMixins.ResolveStrategy.Implicit);
TimetableLog.Info($"WireUpControls found {wiredMembers.Length} members to wire.");
foreach (PropertyInfo member in wiredMembers)
{
    TimetableLog.Info($"  {member.Name} -> resource '{member.GetResourceName()}'.");
}
WireUpControls found 3 members to wire.

GetWireUpMembers returns the properties a strategy would wire without wiring them. GetResourceName reads the resource name one property resolves to. Both are useful for a diagnostic like the one above. The activity's layout, activity_main.xml, declares the three resources those properties resolve to:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  <ReactiveUI.Documentation.PlatformAndroid.LessonCardView
      android:id="@+id/featuredLessonCard"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:padding="8dp" />
  <FrameLayout
      android:id="@+id/badgeContainer"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:padding="8dp" />
  <LinearLayout
      android:id="@+id/peeksRow"
      android:orientation="horizontal"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />
  <androidx.recyclerview.widget.RecyclerView
      android:id="@+id/lessonsRecyclerView"
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:layout_weight="1" />
  <FrameLayout
      android:id="@+id/detailContainer"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />
</LinearLayout>

LessonsRecyclerView, BadgeContainer and DetailContainer are the three writable View-typed properties MainActivity declares. FeaturedLessonCard and PeeksRow are assigned separately in code. So Implicit wires only the properties the activity actually declares as View subtypes. LessonCardView, a compound view inflated into the top of that layout, wires its own two labels the same way, from inside its own constructor:

{
    Orientation = Orientation.Vertical;
    LayoutInflater.From(context)!.Inflate(Resource.Layout.lesson_card, this, true);

    // Implicit strategy: SubjectLabel and RoomLabel are wired to the like-named ids merged into this view.
    this.WireUpControls();
}
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
  <TextView
      android:id="@+id/cardSubjectLabel"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:textStyle="bold" />
  <TextView
      android:id="@+id/cardRoomLabel"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />
</merge>

LessonViewHolder, one row of the lessons RecyclerView, wires SubjectLabel and RoomLabel the same way against the inflated row view, from its constructor body:

{
    // Implicit strategy: any writable View-typed property (SubjectLabel, RoomLabel) is wired to the
    // like-named resource in the inflated row layout.
    this.WireUpControls();

A fragment calls a two-argument overload instead, since it wires against its inflated view rather than itself. LessonNotesFragment uses the convenience form of that overload, which takes only the view and applies Implicit without naming it:

View view = inflater!.Inflate(Resource.Layout.fragment_lesson_notes, container, false)
    ?? throw new InvalidOperationException("Inflating fragment_lesson_notes produced no view.");

// The convenience overload: no resolve strategy to state, since Implicit is what most fragments want.
AndroidX.ControlFetcherMixins.WireUpControls(this, view);

The core, non-AndroidX ReactiveUI.ControlFetcherMixins declares the same two overloads for the platform's own Android.App.Fragment. This page does not show them. Attaching a classic Fragment to an activity needs the classic Activity.FragmentManager. The Android platform itself deprecated that at API level 28, and the timetable app targets a newer API than that.

Explicit opt-in

Under ExplicitOptIn, only a property that carries [WireUpResource] is wired. Every other property is ignored, with no type check. [WireUpResource] takes an optional resource name, for a property whose name does not match its resource. AbsenceStatusView, a compound view, opts in its one label from its constructor body:

{
    LayoutInflater.From(context)!.Inflate(Resource.Layout.absence_status, this, true);

    // ExplicitOptIn: only StatusText, which carries [WireUpResource], is wired.
    this.WireUpControls(ControlFetcherMixins.ResolveStrategy.ExplicitOptIn);
}
[WireUpResource("statusText")]
public TextView? StatusText { get; set; }

LessonDetailFragment opts in two labels whose names differ from their resources, so each one names its resource explicitly:

[WireUpResource("detailSubject")]
public TextView? SubjectLabel { get; set; }

[WireUpResource("detailRoom")]
public TextView? RoomLabel { get; set; }

A fragment wires against its inflated view rather than itself, so it calls the two-argument overload. This excerpt also shows the overload's non-generic-caller form, called directly on AndroidX.ControlFetcherMixins instead of as an extension method:

AndroidX.ControlFetcherMixins.WireUpControls(this, view, ControlFetcherMixins.ResolveStrategy.ExplicitOptIn);

[WireUpResource]'s constructor argument is stored on ResourceNameOverride. GetResourceName() reads it as a fallback when a property carries no override. LessonDetailFragment also reads it directly, off the attribute itself:

// The name SubjectLabel was wired under: WireUpResourceAttribute.ResourceNameOverride, read directly
// rather than through GetResourceName().
WireUpResourceAttribute? subjectAttribute = typeof(LessonDetailFragment)
    .GetProperty(nameof(SubjectLabel))!
    .GetCustomAttribute<WireUpResourceAttribute>();
TimetableLog.Info($"SubjectLabel's resource name override: {subjectAttribute?.ResourceNameOverride}.");
SubjectLabel's resource name override: detailSubject.

The fragment's layout carries a third control, detailIcon, that no property wires. GetControl fetches it directly instead, resolving it against a View and the fragment's assembly:

View? icon = view.GetControl(GetType().Assembly, "detailIcon");
TimetableLog.Info(icon is null ? "detailIcon was not found." : "detailIcon resolved directly with GetControl.");
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">
  <TextView
      android:id="@+id/detailSubject"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:textStyle="bold" />
  <TextView
      android:id="@+id/detailRoom"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />
  <ImageView
      android:id="@+id/detailIcon"
      android:layout_width="24dp"
      android:layout_height="24dp"
      android:contentDescription="@null" />
</LinearLayout>

Explicit opt-out

Under ExplicitOptOut, every writable View-typed property is wired unless it carries [IgnoreResource]. AbsenceActivity uses it, and excludes a label its layout has no resource for:

// ExplicitOptOut: every View-typed property is wired except those carrying [IgnoreResource].
this.WireUpControls(ControlFetcherMixins.ResolveStrategy.ExplicitOptOut);

// A control fetched directly, without a matching property, to show GetControl used on its own.
View? extraLabel = this.GetControl("extraLabel");
TimetableLog.Info(extraLabel is null ? "extraLabel was not found." : "extraLabel resolved directly with GetControl.");
[IgnoreResource]
public TextView? UnusedLabel { get; set; }

Without [IgnoreResource], ExplicitOptOut would try to wire UnusedLabel to a resource named unusedlabel and throw a MissingFieldException, since activity_absence.xml declares no such resource. GetControl above fetches extraLabel directly on the activity, the same way LessonDetailFragment fetched detailIcon on a View. Called with no argument, it resolves the resource named after the calling member instead.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">
  <TextView
      android:id="@+id/statusLabel"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />
  <TextView
      android:id="@+id/extraLabel"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />
  <ReactiveUI.Documentation.PlatformAndroid.AbsenceStatusView
      android:id="@+id/absenceStatusView"
      android:layout_width="match_parent"
      android:layout_height="wrap_content" />
</LinearLayout>

Wiring without reflection

LessonCountBadgeHost, a host with no view model, wires its one label through the auto-wireup constructor of LayoutViewHostUnsafe instead of a call to WireUpControls in its own body. Its constructor passes attachToRoot: false, performAutoWireup: true and resolveStrategy: ControlFetcherMixins.ResolveStrategy.Implicit to the base constructor. That base constructor calls the ILayoutViewHost overload of WireUpControls itself once inflation finishes. The constructor's own body adds nothing further. ReactiveViewHostUnsafe<TViewModel> offers the same constructor for a host with a view model. FirstLessonPeekHost passes the same two flags to it. The reflection-free LayoutViewHost and ReactiveViewHost<TViewModel> have no such constructor.

A host that wires its child without reflection, and stays trim- and AOT-safe, uses the bind-callback constructor instead and calls FindViewById itself. WeekdayViewHost, one page of the weekday pager, passes bind: static (host, view) => ((WeekdayViewHost)host)._weekdayLabel = view.FindViewById<TextView>(Resource.Id.weekdayLabel) to the base constructor this way.

Every WireUpControls overload, and GetControl, carries RequiresUnreferencedCode and RequiresDynamicCode. Both reflect over your assembly's generated resource type to resolve a name to an integer ID. The bind-callback constructor above needs neither. MissingFieldException is what any WireUpControls overload throws when a property cannot be resolved to a matching resource, whichever strategy is in effect.

See Android for the rest of what ReactiveUI.AndroidX adds: activation, lists, paging, dialogs, preferences and service binding.