Skip to content

Instantly share code, notes, and snippets.

@molnard
Created September 3, 2019 12:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save molnard/14e114d9921c09d8331e5492b7c7af09 to your computer and use it in GitHub Desktop.
Save molnard/14e114d9921c09d8331e5492b7c7af09 to your computer and use it in GitHub Desktop.

Wasabi Coding Guidelines for UI

Example ViewModel Class

  public class MyViewModel : ReactiveObject, IDisposable
	{
		private CompositeDisposable Disposables { get; } = new CompositeDisposable();
		private string _myProperty;
		private bool _myConfirmed;
		private ObservableAsPropertyHelper<bool> _confirmed;
		private SmartCoin Model { get; }

		public string MyProperty
		{
			get => _myProperty;
			set => this.RaiseAndSetIfChanged(ref _myProperty, value);
		}

		public bool MyConfirmed
		{
			get => _myConfirmed;
			set => this.RaiseAndSetIfChanged(ref _myConfirmed, value);
		}

		public ReactiveCommand MyCommand { get; }
		public ReactiveCommand MySecondCommand { get; }

		public MyViewModel()
		{

			this.WhenAnyValue(x => x.MyProperty)
				.Subscribe(myProperty =>
				{
				});

			_confirmed = Model.WhenAnyValue(x => x.Confirmed)
				.ToProperty(this, x => x.MyConfirmed, scheduler: RxApp.MainThreadScheduler)
				.DisposeWith(Disposables);

			Model.WhenAnyValue(x => x.IsBanned, x => x.SpentAccordingToBackend)
				.ObserveOn(RxApp.MainThreadScheduler)
				.Subscribe(_ =>
				{
				})
				.DisposeWith(Disposables);

			Dispatcher.UIThread.PostLogException(() =>
			{
				// Do something on the UI thread. App-crash exception handler built-in.
			});

			Observable.FromEventPattern(
				Global.ChaumianClient,
				nameof(Global.ChaumianClient.StateUpdated))
				.ObserveOn(RxApp.MainThreadScheduler)
				.Subscribe(_ =>
				{
				}).DisposeWith(Disposables);

			IObservable<bool> isCoinListItemSelected = this.WhenAnyValue(x => x.MyProperty).Select(myProperty => myProperty != null);

			MyCommand = ReactiveCommand.Create(() =>
			{

			}, isCoinListItemSelected);

			MyCommand.ThrownExceptions
				.Subscribe(ex => Logging.Logger.LogWarning<MyViewModel>(ex));

			MySecondCommand = ReactiveCommand.CreateFromTask(async () =>
			{
				await Task.Delay(100);
			}, isCoinListItemSelected);

			Observable.FromEventPattern(Global.WalletService.Coins, nameof(Global.WalletService.Coins.CollectionChanged))
				.Merge(Observable.FromEventPattern(Global.WalletService, nameof(Global.WalletService.NewBlockProcessed)))
				.Merge(Observable.FromEventPattern(Global.WalletService, nameof(Global.WalletService.CoinSpentOrSpenderConfirmed)))
				.Throttle(TimeSpan.FromSeconds(5))									
				.ObserveOn(RxApp.MainThreadScheduler)
				.Subscribe(_ => 
				{

				})
				.DisposeWith(Disposables);


		}

		#region IDisposable Support

		private volatile bool _disposedValue = false;

		protected virtual void Dispose(bool disposing)
		{
			if (!_disposedValue)
			{
				if (disposing)
				{
					Disposables?.Dispose();
				}
				Disposables = null;

				_disposedValue = true;
			}
		}

		~MyViewModel()
		{
			Dispose(false);
		}

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		#endregion IDisposable Support
	}

Detecting the change of a Property

You can put the following into the constructor of the ViewModel. MyProperty should use this.RaiseAndSetIfChanged() to notify the change. If you're a VM and you e.g. WhenAnyValue against your own property, there's no need to clean that up because that is manifested as the VM having a reference to itself.

	this.WhenAnyValue(x => x.MyProperty)
		.Subscribe(myProperty =>
		{
		});

If you are referencing other object you should dispose your subscription. Use .DisposeWith() method.

	Model.WhenAnyValue(x => x.IsBanned, x => x.SpentAccordingToBackend)
		.ObserveOn(RxApp.MainThreadScheduler)
		.Subscribe(_ =>
		{
		})
		.DisposeWith(Disposables);

Event subscriptions

Reactive generates an observable from the event and from then you can use all the toolset of Rx. Regarding the disposal it is pretty similar. If a component exposes an event and also subscribes to it itself, it doesn't need to unsubscribe. That's because the subscription is manifested as the component having a reference to itself.

	Observable.FromEventPattern(CoinList, nameof(CoinList.SelectionChanged)).Subscribe(_ => SetFeesAndTexts());

	Observable.FromEventPattern(
		Global.ChaumianClient,
		nameof(Global.ChaumianClient.StateUpdated))
		.ObserveOn(RxApp.MainThreadScheduler)
		.Subscribe(_ =>
		{
			RefreshSmartCoinStatus();
		}).DisposeWith(Disposables);

More examples

Expose Model Property to View

When a property's value depends on another property, a set of properties, or an observable stream, rather than set the value explicitly, use ObservableAsPropertyHelper with WhenAny wherever possible.

	//Define class members.
	private ObservableAsPropertyHelper<bool> _confirmed;
	public bool Confirmed => _confirmed?.Value ?? false;

	//Initialize PropertyHelper.
	_confirmed = Model.WhenAnyValue(x => x.Confirmed).ToProperty(this, x => x.Confirmed).DisposeWith(Disposables);
	
	//Initialize PropertyHelper if invoked from not UI thread
	_confirmed = Model.WhenAnyValue(x => x.Confirmed).ToProperty(this, x => x.Confirmed, scheduler:RxApp.MainThreadScheduler).DisposeWith(Disposables);

ReactiveCommand

Prefer binding user interactions to commands rather than methods.

	//Create Observable<bool> on MyProperty for CanExecute. Null detection always tricky but following code will handle that.
	IObservable<bool> isCoinListItemSelected = this.WhenAnyValue(x => x.MyProperty).Select(myProperty => myProperty != null);

	//Define command with CanExecute. 
	MyCommand = ReactiveCommand.Create(() =>
	{

	}, isCoinListItemSelected);

	//Define command from Task
	MySecondCommand = ReactiveCommand.CreateFromTask(async () =>
	{
		await Task.Delay(100);
	}, isCoinListItemSelected);

Reactive command doesnt have any unmanaged resources. Dispose in Reactive command doesnt mean release unmanaged resources, it simply means unsubscribe. Reactive command is on the same object that is subscribing, so GC will handle everything. So no memory leak here. See this comment from the author of RxUI...

!!!WANTED!!! 10000 Dollar Reward Dead or Alive!

DO NOT use PropertyChanged.

	private void Coin_PropertyChanged(object sender, PropertyChangedEventArgs e)
	{			
		if (e.PropertyName == nameof(CoinViewModel.Unspent))	
		{	
		}
	}

DO NOT use event subscriptions.

	Global.ChaumianClient.StateUpdated += ChaumianClient_StateUpdated;

	if (Global.ChaumianClient != null)
	{
		Global.ChaumianClient.StateUpdated -= ChaumianClient_StateUpdated;
	}

DO NOT do anything in set except RaiseAndSetIfChanged().

	public int FeeTarget
	{
		get => _feeTarget;
		set
		{
			this.RaiseAndSetIfChanged(ref _feeTarget, value);
			Global.UiConfig.FeeTarget = value;
		}
	}

DO NOT use async void Method().

	async void DoSomething()
	{
	
	}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment