ResponsiveTasks_1.md
August 17, 2021 ยท View on GitHub
Part 1 of N: Pages, Views & View Models
The Old Way
A Xamarin.Forms ContentPage or ContentView provide their Content and BindingContext through simple properties. View Models are constructed and assigned as binding contexts, but provide no support whatsoever for the moment of being bound. All of these cases lack Task signatures, so are false roots.
public ContentView AnyContentView()
{
private object _bindingContext;
private object _content;
public AnyContentView()
{
// Can't await, so hacked call using async void
Content = CreateContent().FireAndForget();
}
public object Content
{
get => _content;
set
{
_content = value;
// Can't await a property, so calling an extension
// to cheat around it.
ContentChangedTask.FireAndForget();
}
}
public object BindingContext
{
set
{
_bindingContext = value;
// Same problem as with Content.
BindingContextChangedTask.FireAndForget();
}
private async Task<View> CreateContent()
{
var content = new StackLayout();
// Any async code here
await Task.Delay(100).WithouChangingContext();
return content;
}
private Task ContentChangedTask()
{
}
private Task BindingContextChangedTask()
{
}
}
public static class Utils
{
private static async void FireAndForget(this Task task)
{
try
{
await task().WithouChangingContext();
}
catch (Exception ex)
{
}
}
}
These are set like so:
public ContentPage AnyPage
{
public AnyPage()
{
var contentView = AnyContentView();
// Not obviously wrong, but the constructor at
// AnyContentView illegally creates a false TPL root.
// Everything created there will be 'out of time'.
Content = contentView;
// Not obviously wrong, but the content view needs to run
// the binding context change async to handle various tasks,
// so this hides another false root and another bad process.
contentView.BindingContext = BindingContext;
// Any tasks run from here will add further to the damage,
// since it is identical to AnyContentView.
}
}
The New Way: Responsive Tasks
Key concepts:
- Don't run a task from an illegal async void root, including a constructor or a property setter.
- Prefer ContentViews to Views. Setting Content is critical to proper TPL flow.
Instead:
- Replace void property setters with Tasks
- Prefer content setting to construction
- Prefer binding changes to other odd cases
These are related topics. We carefully manage key properties such as ContentView/Content (and the PageView.Content) as well as the BindingContext:
// ***
// For discussion only; verbose elements are missing.
// See the demo project for the complete code.
// ***
// At the constructor, subscribe to content and binding context tasks
public MyContentPage()
{
CallPrepare();
PostBindingTasks.AddIfNotAlreadyThere(this, HandlePostBindingTask);
PostContentTasks.AddIfNotAlreadyThere(this, HandlePostContentTask);
}
// Hide misbehaved void setters and overrides
public new object BindingContext => base.BindingContext;
public new View Content => base.Content;
protected sealed override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
}
// Set the binding context using a Task.
public async Task SetBindingContextSafely(object context)
{
base.BindingContext = context;
// Raise the current class's post binding context tasks
await RunPostBindingTasks(this).WithoutChangingContext();
if (
RunSubBindingContextTasksAfterAssignment &&
BindingContext.IsNotNullOrDefault() &&
BindingContext is IProvidePostBindingTasks bindingContextAsRunningPostBindingTasks
)
{
// Raise the *view model's* post binding context tasks
await bindingContextAsRunningPostBindingTasks.RunPostBindingTasks(this).WithoutChangingContext();
}
await ConsiderSettingContentBindingContext();
}
// Allow derivers to override the binding context Task handler.
// Alternately, they can subscribe to the PostBindingTasks directly.
protected virtual Task HandlePostBindingTask(IResponsiveTaskParams paramdict)
{
return Task.CompletedTask;
}
// ***
// Similar code for setting Content
// ***
Consume these new tasks with other tasks to maintain the TPl chain of control
This linkage tends to run through many layers in the UI. Eventually it reaches the App level, the headwaters of the TPL system, where we finally must utilize a void TPL root:
// Seal the App to prevent derivation oddities with TPL
public sealed class App : Application
{
public App()
{
// This is the only page; all stages are views within this page.
var masterPage = new MyContentPage();
// IMPORTANT
// This is the TPL root.
// The app is not inheritable, so does not have side-effects.
// The startup process and the fade in process also do not compete with each other.
MainThread.BeginInvokeOnMainThread(
async () =>
{
MainPage = masterPage;
// These are ResponsiveTasks methods that safely wait for completion of direct ---
// and branching -- tasks before proceeding.
await ResponsiveTaskHelper.AwaitClassPostContent(masterPage).WithouChangingContext();
await ResponsiveTaskHelper.AwaitClassAndViewModelPostBinding(masterPage, viewModel).WithouChangingContext();
});
}
}