Objects that want to notify other parts of the system when their properties change must implement INotifyPropertyChanged
.
INotifyPropertyChanged
look like:
public interface INotifyPropertyChanged
{
// Occurs when a property value changes.
event PropertyChangedEventHandler PropertyChanged;
}
Frameworks that support binding (e.g. Xamarin Forms) have a binding class that hooks into/provides the implementation of the event. So when you fulfill your side of the interface by triggering the event, what you're actually doing is running the binding class' method that (probably) updates relevant UI elements/views.
In the most raw form, you could create trigger the event at the appropriate time:
public class MyClassThatNotifiesOthers
{
private event PropertyChangedEventHandler EventHandlerWhenNameChanges;
private string _name;
public string Name
{
get => this._name;
set
{
this._name = value;
this.EventHandlerWhenNameChanges?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
The most basic (and bad) way of binding a view to some object property is to do it in the view's code-behind.
So, say I am creating a HomePage
component.
It will extend from Xamarin.Forms.ContentPage
.
All Pages are subclasses of Xamarin.Forms.BindableObject
, which implements INotifyPropertyChanged
.
BindableObject
implements INotifyPropetyChanged
like so:
public class BindableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
//...
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
That means we have an easy way for our components to logically implement INotifyPropertyChanged
, via BindableObject.OnPropertyChanged()
.
We make our viewmodels extend from that base class and call that method when its properties actually change. Then when we bind views to the viewmodel's properties, Xamarin can handle the visual updating.
public class MyViewModel : BindableObject
{
private string displayString;
public string DisplayString
{
get => this.displayString;
set
{
if (this.displayString != value)
{
this.displayString = value;
this.OnPropertyChanged(nameof(this.DisplayString));
}
}
}
}
Ok Xamarin provides an implementation of INotifyProperyChanged
, so why do we use Prism and/or ReactiveUI?
Prism, ReactiveUI, and MVVMHelpers all offer their own base classes that implement INotifyProperyChanged
.
Each also comes with more OOTB features, but they are very similar in how they go about INotifyProperyChanged
implementation.
Library | class to extend | extended class method to call in your VM's setter to trigger the INotifyProperyChanged.PropertyChanged event |
---|---|---|
Xamarin.Forms | BindableObject |
OnPropertyChanged() |
ReactiveUI | ReactiveObject |
RaiseAndSetIfChanged() |
Prism | BindableBase |
SetProperty() |
This is a Fody thing. Specifically the "Reactive" add-in for Fody. Fody is an extensible tool for weaving .NET assemblies, and they'll inject INotifyPropertyChanged code into properties at compile time for you. It reduces rendundant boilerplate code, simplifying this...
private string name;
public string Name
{
get => name;
set => this.RaiseAndSetIfChanged(ref name, value);
}
...into this
[Reactive]
public string Name { get; set; }
- You can bind the properties of a view to the properties of another view in the same page
- Microsoft's example is a slider with a label. The text on the label is bound to the value of the slider: if the slider goes up, the text automatically updates to show the value
- This is usefu/necessary in a
ListView
- The binding context of each item in the list is set to the appropriate element in
ListView.ItemsSource
- If you want each repeated item to reference the same binding context as the
ListView
's (which is probably the viewmodel of that page), do it like so: {Binding Source={x:Reference MyListView}, Path=BindingContext}
is like saying "this binding comes from the 'BindingContext' property of the 'MyListView' view/element"- See Microsoft's section on this as well as this SO answer
- The binding context of each item in the list is set to the appropriate element in
<ListView x:Name="MyListView"
ItemsSource="{Binding MyBaseballTeams}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding TeamName}" />
<Button BindingContext="{Binding Source={x:Reference MyListView}, Path=BindingContext}"
Command="{Binding ViewTeamCommand}"
Text="View Info" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Hmm okay, but zoom out a little and I don't see where the ListView
's binding context is set to anything...
- This is where
ReactiveUI.ContentView<VM>
comes in - I think this automagically wires up the view to use the specified viewmodel parameter
All of our viewmodels extend from ReactiveUI.ReactiveObject
.
The intro and Xamarin Forms docpages have more info.
this.OneWayBind(ViewModel,
viewModel => viewModel.Person.Name,
view => view.Name.Text);
// OR
this.WhenAnyValue(x => x.ViewModel.Person.Name)
.BindTo(this, view => view.Name.Text);
TODO - make note on .Bind()
(two-way)
We use Prism for navigation. There's a three-step (plus some setup) process for getting started:
- Make
App
extendPrism.DryIoc.PrismApplication
- ctors
OnInitialized()
should callInitializeComponent()
and navigate to first page (maybe revisit this part after setting everything else up)
public partial class App : Prism.DryIoc.PrismApplication
{
public App() : this(null) { }
public App(IPlatformInitializer initializer) : base(initializer) { }
protected override void OnInitialized()
{
InitializeComponent();
var result = this.NavigationService.NavigateAsync("SplashPage");
}
}
- Register pages that should be navigable
- do this in
App
's RegisterForNavigation - the overload of
RegisterForNavigation<TPage, TViewModel>()
shown below handles setting the binding context of the specified page!
- do this in
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<SplashPage, SplashPageViewModel>();
containerRegistry.RegisterForNavigation<HomePage, HomePageViewModel>();
}
- Inject an
INavigationService
into whichever viewmodels will be doing navigation:
public class SplashPageViewModel : ReactiveObject
{
private readonly INavigationService _navigationService;
public SplashPageViewModel(INavigationService navigationService)
{
this._navigationService = navigationService;
}
}
- Perform navigation with either relative or absolute paths:
- kind of important to handle failed navigation - otherwise it fails silently...
private async void NavigateToHome()
{
var result = await this._navigationService.NavigateAsync("HomePage");
if (!result.Success)
{
Console.WriteLine(result.Exception);
}
}
- When registering pages for navigation, you can provide a different key/identifier, but using the page/class name seems better
- There are two types of navigation paths
- Relative: subsequent navs will create a stack (e.g.
NavigateAsync("SomeView")
) - Absolute: prefixing a nav with backslash will reset the stack (e.g.
NavigateAsync("/SomeView")
)
- Relative: subsequent navs will create a stack (e.g.
- That's it!
Microsoft came out with Reactive Extensions ("ReactiveX") and its initial implementation on .NET. Today, ReactiveX has implementations in most major languages.
Similar to INotifyPropertyChanged
is INotifyCollectionChanged
. It defines an event that should be raised "when the underlying collection changes".
The provided implementation is ObservableCollection<T>
which "Represents a dynamic data collection that provides notifications when items get added or removed, or when the whole list is refreshed".
The expected behavior might be that if you change a property on an object in the collection, the event would be raised. After all, you are modifying the collection, right? Nope - only collection-related actions (add, remove, clear) trigger the event.
DynamicData (Github) does two things:
- Raises the
CollectionChanged
event when an item in the collection is changed - "Expose changes to the collection via an observable change set. The resulting observable change sets can be manipulated and transformed using Dynamic Data’s robust and powerful array of change set operators". In other words, a LINQ-like API to take action on observables
Using DynamicData is also a multi-step process:
- Define a change set cache (optionally with a unique key selector)
private SourceCache<Tweet, Guid> _tweetCache = new SourceCache<Tweet, Guid>(x => x.UniqueId);
- Define an ObservableCollection
private readonly ReadOnlyObservableCollection<Tweet> _tweets;
- Expose the collection with a property
public ReadOnlyObservableCollection<Tweet> Tweets => this._tweets;
- In the constructor, set up the cache and connect the observable
public TwitterFeedViewModel()
{
IObservable<IChangeSet<Tweet, Guid>> changeSet = this._tweetCache
.Connect()
.RefCount();
changeSet
.Bind(out this._tweets) // step 2
.DisposeMany()
.Subscribe()
.DisposeWith(this.Disposables);
}
- Bind a view to the exposed property (either XAML or code-behind):
<ListView
x:Name="TweetListView"
ItemsSource="{Binding Tweets}">
</ListView>
or
public MyTweetView()
{
this.WhenAnyValue(view => view.ViewModel.Tweets)
.BindTo(this, view => view.TweetListView.ItemsSource)
.DisposeWith(this.Disposables);
}