-
Notifications
You must be signed in to change notification settings - Fork 4
Models ViewStates MVP Binding
MemEngine360 uses a mostly Code-Behind architecture; like MVP, except the views themselves are presenters. There are some places where MVP is used.
- Models contain the properties and fire events when a property changes. When an event fires, it may mean different similar properties change
- The views add/remove event handlers on the models, and update the appropriate controls.
In some places, and soon to be more, we use ViewStates. These contain UI specific information that we carry along side a model.
For example, the class TaskSequenceManagerViewState contains a selection manager for TaskSequence objects, because selection has no business being inside of the TaskSequenceManager class.
It also contains the ConditionHost property, which is used by the UI to present the list of conditions. This property is updated when the primary selected sequence or operation changes.
ViewStates are storable in different ways:
- Internal field in a model
- Model implements IComponentManager, and the ViewState object accesses itself from there
- ConditionalWeakTable<MyModel, MyModelViewState>
- ... and more
Option 1 is by far the easiest but has a separation of concern problem (although since it's internal, we can probably get away with it), and doesn't work with custom plugin-defined view states.
Option 2 is the best balance between SoC and flexibility, since a plugin can use custom view states for a model, which isn't possible with internal fields.
Option 3 is very flexible since you can attach anything to a model, but at the cost of weak referenced keys, which could be an issue with 10,000s of weak referenced objects.
The IBinder framework, found in the namespace PFXToolKitUI.Avalonia.Bindings, aims to massively simplify binding model values to controls, and there's a lot of different binders for different scenarios.
When a binder becomes fully attached (as in, a control and model are attached), the first thing it does is update the control.
These are some of the common binders:
The most common binder used. Listens to a model's event and fires the updateControl callback. Uses the same updateControl/updateModel mechanism
Similar to EventUpdateBinder but can listen to multiple events
Effectively binds an avalonia property to a CLR property. Uses two Action<IBinder<TModel>> called updateControl (required) and updateModel (optional; maybe you want the control to be read only so it can't change the model). When updateModel is not used, consider EventUpdateBinder instead
TextBox specific binders use a Func<IBinder<TModel>, string> to get the text from a model, and an async function Func<IBinder<TModel>, string, Task> to try and update the model based on the text in the text box, or do something like show the user a dialog containing the problem with the text.
We use this for the Start and Length fields in the Memory Scanning Options panel.
These are special radio button binders used to "assign" an enum value to a radio button so that, when that radio button becomes checked, the model's enum property is set to whatever value was assigned to that radio button.
Similar to AvaloniaPropertyToEventPropertyBinder but uses getter/setter functions to get/set the model value.
Here are some examples of how to use binders
In this case, since we only want to update the TextBlock from the model, we'd use EventUpdateBinder.
A simple way to use a binder is to attach/detach the control in the Loaded/Unloaded methods. Attaching/Detaching the model depends on how you want to associate the model with a control. If it's a standalone control, it might be an AvaloniaProperty, in which case, you'd attach/detach the model in the property change handler.
private readonly IBinder<MyDataModel> displayNameBinder =
new EventUpdateBinder<MyDataModel>(
nameof(MyDataModel.DisplayNameChanged),
(b) => ((TextBlock) b.Control).Text = b.Model.DisplayName);`
... define model property here perhaps ...
protected override void OnLoaded(RoutedEventArgs e) {
base.OnLoaded(e);
this.displayNameBinder.AttachControl(this);
}
protected override void OnUnloaded(RoutedEventArgs e) {
base.OnUnloaded(e);
this.displayNameBinder.DetachControl();
}
protected void OnModelChanged(MyDataModel? oldModel, MyDataModel? newModel) {
this.displayNameBinder.SwitchModel(newModel);
}
Or if you don't want to use OnLoaded/OnUnloaded, you can do this instead:
protected void OnModelChanged(MyDataModel? oldModel, MyDataModel? newModel) {
if (oldModel != null)
this.displayNameBinder.Detach();
if (newModel != null)
this.displayNameBinder.Attach(this, newModel);
}
-
Home
- Connect to a console
- Scanning Options
- Scan results & Saved Address Table
- Remote Commands
- Memory Dump
- Tools
- Preferences/App Settings
-
API
- Making a custom connection
- Busy Tokens
- Models, ViewStates, MVP & Binding
- Plugins
- Config Pages
- Brushes and Icons
- Data Manager, Context Data and Data Keys
- Commands and Shortcuts
- Context Menus
- Windows and Dialogs