Coder Social home page Coder Social logo

diabloapp's Introduction

Binding

Setting things up and background reading

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()

Alright, but what's this [Reactive] attribute on all the bound-to properties?

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; }

Vanilla Xamarin.Forms binding


View-to-View Binding with Vanilla Xamarin

  • 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
<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

Binding with ReactiveUI

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)



Navigation

Prism Navigation

We use Prism for navigation. There's a three-step (plus some setup) process for getting started:

  1. Make App extend Prism.DryIoc.PrismApplication
    • ctors
    • OnInitialized() should call InitializeComponent() 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");
    }
}
  1. 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!
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterForNavigation<SplashPage, SplashPageViewModel>();
    containerRegistry.RegisterForNavigation<HomePage, HomePageViewModel>();
}
  1. 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;
    }
}
  1. 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);
    }
}

Notes on Prism navigation

  • 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"))
  • That's it!


Observables

History Lesson

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.

Enter DynamicData

DynamicData (Github) does two things:

  1. Raises the CollectionChanged event when an item in the collection is changed
  2. "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:

  1. Define a change set cache (optionally with a unique key selector)
private SourceCache<Tweet, Guid> _tweetCache = new SourceCache<Tweet, Guid>(x => x.UniqueId);
  1. Define an ObservableCollection
private readonly ReadOnlyObservableCollection<Tweet> _tweets;
  1. Expose the collection with a property
public ReadOnlyObservableCollection<Tweet> Tweets => this._tweets;
  1. 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);
}
  1. 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);
}

diabloapp's People

Contributors

chriwong avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.