Permalink
Browse files

Update the sample to reflect best practices

  • Loading branch information...
paulcbetts committed Jan 16, 2011
1 parent b00354d commit 64caefdae70def52e9d328f2a85210196c60333d
@@ -1,24 +1,30 @@
-using ReactiveUISample;
+using ReactiveUI;
+using ReactiveUISample;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Windows;
namespace ReactiveUISample.Tests
{
[TestClass()]
- public class AddPersonViewModelTest
+ public class AddPersonViewModelTest : IEnableLogger
{
[TestMethod]
public void OkButtonShouldBeDisabledWhenSomeOfThePropertiesAreInvalid()
{
var target = new AddPersonViewModel();
- target.Person = new PersonEntry() {AwesomenessFactor = 50, Name = "Bob", PhoneNumber = "NotAPhoneNumber"};
+ target.Person = new PersonEntry() {AwesomenessFactor = 50, Name = "Steve", PhoneNumber = "NotAPhoneNumber"};
Assert.IsFalse(target.OkCommand.CanExecute(null));
+ this.Log().Warn("Setting Phone Number");
target.Person.PhoneNumber = "555.333.1222";
-
+
+ this.Log().Info(target.SetImageViaFlickr.CanExecute(null));
+ this.Log().Info(target.Person.IsObjectValid());
+
+ this.Log().Warn("About to Assert");
Assert.IsTrue(target.OkCommand.CanExecute(null));
target.Person.Name = "";
@@ -80,13 +80,6 @@ public void RemovePersonTest()
target.RemovePerson.Execute(null);
Assert.AreEqual(0, target.People.Count);
Assert.IsFalse(target.RemovePerson.CanExecute(null));
-
- // Now try removing explicitly
- target.AddPerson.Execute(to_add);
- Assert.AreEqual(1, target.People.Count);
- Assert.IsTrue(target.RemovePerson.CanExecute(to_add));
- target.RemovePerson.Execute(to_add);
- Assert.AreEqual(0, target.People.Count);
}
}
}
@@ -1,23 +1,13 @@
using System;
using System.Collections.Generic;
-using System.Text;
+using System.ComponentModel.Composition;
+using System.Globalization;
+using System.Linq;
+using System.Web;
using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
using System.Windows.Media.Imaging;
-using System.Windows.Shapes;
-using System.Threading;
using System.Xml.Linq;
-using System.Linq;
-using System.Globalization;
-using System.Web;
-using System.Threading.Tasks;
-using System.ComponentModel.Composition;
using ReactiveUI;
-using System.Concurrency;
using ReactiveUI.Xaml;
namespace ReactiveUISample
@@ -109,11 +99,6 @@ public PersonEntry Prompt(object sender, object parameter)
public class AddPersonViewModel : ReactiveValidatedObject
{
- public AddPersonViewModel()
- {
- setupCommands();
- }
-
/*
* Our Model
*/
@@ -142,9 +127,8 @@ public AddPersonViewModel()
get { return _SpinnerVisibility.Value; }
}
- protected void setupCommands()
+ public AddPersonViewModel()
{
-
/* COOLSTUFF: How to do stuff in the background
*
* ReactiveAsyncCommand is similar to ReactiveCommand, except that
@@ -163,14 +147,14 @@ protected void setupCommands()
* on the UI thread.
*/
- SetImageViaFlickr = new ReactiveAsyncCommand();
+ this.SetImageViaFlickr = new ReactiveAsyncCommand();
// NB: The regular RNG isn't thread-safe, and will be very un-random
// if we use it from more than one thread - however, RAC guarantees
// that only one will be running at a time.
var rng = new Random();
- var results = SetImageViaFlickr.RegisterAsyncFunction(_ => {
+ var results = this.SetImageViaFlickr.RegisterAsyncFunction(_ => {
var images = findRandomFlickrImages("Face");
if (images == null || images.Length == 0)
return null;
@@ -183,8 +167,7 @@ protected void setupCommands()
// This will be on the UI thread, so we're safe to set WPF properties
// here
- results.Subscribe(s => Person.Image = new BitmapImage(new Uri(s)),
- ex => this.Log().Error("Oh no!", ex));
+ results.Subscribe(s => this.Person.Image = new BitmapImage(new Uri(s)));
/* COOLSTUFF: The Reactive Extensions
@@ -200,14 +183,24 @@ protected void setupCommands()
* that we change a property since it's a ReactiveObservableObject.
*/
- _SpinnerVisibility = this.ObservableToProperty(
- SetImageViaFlickr.CanExecuteObservable.Select(x => x ? Visibility.Collapsed : Visibility.Visible),
- x => x.SpinnerVisibility, Visibility.Collapsed);
-
- OkCommand = new ReactiveCommand(
- SetImageViaFlickr.CanExecuteObservable.CombineLatest(Person.Changed.Select(x => Person.IsObjectValid()),
- (flickr_isnt_running, is_valid) => flickr_isnt_running && is_valid));
+ this._SpinnerVisibility = this.SetImageViaFlickr.CanExecuteObservable
+ .Select(x => x ? Visibility.Collapsed : Visibility.Visible)
+ .ToProperty(this, x => x.SpinnerVisibility, Visibility.Collapsed);
+
+ // NB: Why the rigamarole here? Whenever someone sets the Person,
+ // we need to redo the OkCommand, since OkCommand depends on Person
+ this.ObservableForProperty(x => x.Person).Subscribe(_ => {
+ var canHitOk = Observable.CombineLatest(
+ this.SetImageViaFlickr.CanExecuteObservable.StartWith(true),
+ this.Person.Changed.Select(x => this.Person.IsObjectValid()).StartWith(this.Person.IsObjectValid()),
+ (flickrNotRunning, personIsValid) => flickrNotRunning && personIsValid);
+
+ // We don't actually do anything in the ViewModel when the user hits
+ // Ok, the View is listening to this command and closes the dialog.
+ this.OkCommand = new ReactiveCommand(canHitOk);
+ });
+ this.OkCommand = new ReactiveCommand();
}
/* COOLSTUFF: Always write the sync version first!
@@ -1,19 +1,11 @@
using System;
-using System.Collections.Generic;
-using System.Configuration;
-using System.Data;
-using System.Linq;
-using System.Windows;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
-using System.ComponentModel;
-using System.Collections.ObjectModel;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Input;
using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Windows;
+using System.Windows.Media;
using ReactiveUI;
-using System.Concurrency;
using ReactiveUI.Xaml;
namespace ReactiveUISample
@@ -80,7 +72,6 @@ private void Application_Startup(object sender, StartupEventArgs e)
* or a blocking disk/network call on the RxApp.DeferredScheduler when
* it should've probably be run on the RxApp.TaskpoolScheduler (i.e. not
* on the UI thread). */
-
#if DEBUG
RxApp.EnableDebugMode();
#endif
@@ -95,13 +86,8 @@ private void Application_Startup(object sender, StartupEventArgs e)
[Export(typeof(AppViewModel))]
public class AppViewModel : ReactiveValidatedObject
{
- ObservableCollection<PersonEntry> _People = new ObservableCollection<PersonEntry>();
- public ObservableCollection<PersonEntry> People { get { return _People; } }
+ public ReactiveCollection<PersonEntry> People { get; protected set; }
- public AppViewModel()
- {
- setupCommands();
- }
PersonEntry _SelectedPerson;
public PersonEntry SelectedPerson {
@@ -136,11 +122,13 @@ public AppViewModel()
[Export("RemovePerson", typeof(IReactiveCommand))]
public ReactiveCommand RemovePerson { get; protected set; }
+ [Import(typeof(IPromptForModelDialog<PersonEntry>))]
+ IPromptForModelDialog<PersonEntry> addPersonDialog { get; set; }
+
- /* COOLSTUFF: Setting up Commands
+ /* COOLSTUFF: Setting up Commands and Bindings
*
- * This function must be implemented in every ReactiveObservableObject -
- * this is the place where you can actually implement the commands you
+ * This is the place where you can actually implement the commands you
* declare (or more likely, call functions that implement these
* commands if they are complex).
*
@@ -151,28 +139,31 @@ public AppViewModel()
* reflect that change in the View. When you are disciplined in this
* fashion, it makes testing your code much easier, since your code
* won't depend on being inside a WPF Window / Button / whatever. */
-
- [Import(typeof(IPromptForModelDialog<PersonEntry>))]
- IPromptForModelDialog<PersonEntry> addPersonDialog { get; set; }
-
- protected void setupCommands()
+ public AppViewModel()
{
- AddPerson = ReactiveCommand.Create(null, item => {
- var to_add = (item as PersonEntry) ?? addPersonDialog.Prompt(this, null);
+ People = new ReactiveCollection<PersonEntry>();
- if (to_add == null)
- return;
- if (!to_add.IsObjectValid())
+ AddPerson = new ReactiveCommand();
+ AddPerson.Subscribe(item => {
+ var to_add = (item as PersonEntry) ?? this.addPersonDialog.Prompt(this, null);
+
+ if (to_add == null || !to_add.IsObjectValid())
return;
this.Log().DebugFormat("Adding '{0}'", to_add.Name);
- People.Add(to_add);
+ this.People.Add(to_add);
});
- RemovePerson = ReactiveCommand.Create(param => (param ?? SelectedPerson) != null && People.Count > 0, item => {
- var to_remove = (PersonEntry)item ?? SelectedPerson;
- this.Log().DebugFormat("Removing '{0}'", to_remove.Name);
- People.Remove(to_remove);
+ var canRemovePerson = Observable.CombineLatest(
+ this.ObservableForProperty(x => x.SelectedPerson).Select(x => x.Value != null).StartWith(false),
+ People.CollectionCountChanged.Select(x => x > 0).StartWith(false),
+ (selectedPersonNotNull, collectionNotEmpty) => selectedPersonNotNull && collectionNotEmpty);
+
+ RemovePerson = new ReactiveCommand(canRemovePerson);
+ RemovePerson.Subscribe(_ => {
+ this.Log().DebugFormat("Removing '{0}'", SelectedPerson.Name);
+ People.Remove(SelectedPerson);
+ SelectedPerson = null;
});
}
}
@@ -200,6 +191,7 @@ public class PersonEntry : ReactiveValidatedObject
set { _Image = this.RaiseAndSetIfChanged(x => x.Image, value); }
}
+
/* COOLSTUFF: Data Validation
*
* In addition to providing change notification support,
@@ -254,4 +246,4 @@ public interface IPromptForModelDialog<T>
{
T Prompt(object sender, object parameter);
}
-}
+}
Oops, something went wrong.

0 comments on commit 64caefd

Please sign in to comment.