ReactiveUIã¯æ¬æ ŒçãªMVVMãã¬ãŒã ã¯ãŒã¯ã§ãããã€ã³ãã£ã³ã°ãã«ãŒãã£ã³ã°ãã¡ãã»ãŒãžãã¹ãã³ãã³ããªã©ãã»ãŒãã¹ãŠã®MVVMãã¬ãŒã ã¯ãŒã¯ã®èª¬æã«ããä»ã®åèªããããŸãã WPFãWindowsãã©ãŒã ãUWPãWindows Phone 8ãWindowsã¹ãã¢ãXamarinãªã©ã.NETãååšããã»ãŒãã¹ãŠã®å Žæã§äœ¿çšã§ããŸãã
ãã¡ãããããªãããã§ã«ããã䜿ã£ãçµéšãããã°ãããã§ããªãèªèº«ã®ããã«äœãæ°ãããã®ãèŠã€ããããšã¯ãŸããããŸããã ãã®èšäºã§ã¯ãViewModelã§ã®ããããã£ã®æäœã«é¢ããåºæ¬çãªæ©èœã«ç²Ÿéããå°æ¥ãä»ã®ããèå³æ·±ãè€éãªæ©èœã«å°éããããšãæãã§ããŸãã
ã¯ããã«
ReactiveUIã¯ãªã¢ã¯ãã£ãããã°ã©ãã³ã°ã¢ãã«ãäžå¿ã«æ§ç¯ããã Reactive Extensions ïŒRxïŒã䜿çšããŸãã ãã ãããªã¢ã¯ãã£ãããã°ã©ãã³ã°ã¬ã€ããäœæãããšããç®æšã¯èšå®ããŠããŸãããå¿ èŠãªå Žåã«ã®ã¿ããã®ä»çµã¿ã説æããŸãã ããã«ãåºæ¬çãªæ©èœã䜿çšããããã«ããããã©ããªæªç©ã§ããããæãäžããå¿ èŠãããªãããšãããããŸãïŒãªã¢ã¯ãã£ãããã°ã©ãã³ã°ã ããªãã¯ãã§ã«åœŒã«ç²ŸéããŠããŸãããã€ãã³ãã¯ããã ãã§ãã éåžžããåå¿æ§ããçŸããå Žæã§ãããã³ãŒãã¯éåžžã«ç°¡åã«èªã¿åãããäœãèµ·ããããç解ã§ããŸãã ãã¡ãããã©ã€ãã©ãªïŒããã³Reactive ExtensionsïŒãæ倧éã«äœ¿çšããå Žåããªã¢ã¯ãã£ãã¢ãã«ã«çå£ã«ç²Ÿéããå¿ èŠããããŸãããããã§ã¯åºæ¬ã説æããŸãã
å人çã«ã¯ãReactiveUIã®çŽæ¥çãªæ©èœã«å ããŠãæ§ãããªæ©èœãæ°ã«å ¥ã£ãŠããŸããä»ã®æ©èœã«æ³šæãæã£ãããã¢ããªã±ãŒã·ã§ã³ããã¬ãŒã ã¯ãŒã¯ã«åããããããã«ãæ©èœã®äžéšã®ã¿ã䜿çšã§ããŸãã ããšãã°ãéäºææ§ãè¶ ããŠå®è¡ããããšãªããä»ã®ãã¬ãŒã ã¯ãŒã¯ãšäžŠè¡ããŠé©çšããŸãã ããªãå¿«é©ã§ãã
è»èã«ããšããããŸãã 圌女ã®ååã¯ããã¥ã¡ã³ãã§ãã 圌女ã®ãã¹ãŠãéåžžã«æªãã§ãã äœããããã«ãããŸãããå€ãã®ããŒãžã¯åãªãã¹ã¿ãã§ããããã¹ãŠãéåžžã«ä¹Ÿç¥ããŠããŸãã ããã«ã¯ããã¥ã¡ã³ãããããŸãããåé¡ã¯åãã§ãïŒã¹ã¿ããéçºè ã®ãã£ããããã®ã³ããŒããŒã¹ããããŸããŸãªãœãŒã¹ã®ãµã³ãã«ã¢ããªã±ãŒã·ã§ã³ãžã®ãªã³ã¯ãå°æ¥ã®ããŒãžã§ã³ã®æ©èœã®èª¬æãªã©ã éçºè ã¯StackOverflowã®è³ªåã«çããã®ã«éåžžã«ç©æ¥µçã§ãããéåžžã®ããã¥ã¡ã³ããããã°å€ãã®è³ªåã¯ãããŸããã ããããããã§ã¯ãªãã®ã¯ããã§ã¯ãããŸããã
è°è«ãããããš
詳现ã«ç§»ããŸãããã ãã®èšäºã§ã¯ãViewModelsã®ããããã£ã«é¢ããäžè¬çãªåé¡ãšãReactiveUIã§ã®è§£æ±ºæ¹æ³ã«ã€ããŠèª¬æããŸãã ãã¡ããããã®åé¡ã¯INotifyPropertyChangedã€ã³ã¿ãŒãã§ã€ã¹ã§ãã äœããã®æ¹æ³ã§äœããã®åœ¢ã§è§£æ±ºãããåé¡ã
åŸæ¥ã®å®è£ ãèŠãŠã¿ãŸãããã
private string _firstName; public string FirstName { get { return _firstName; } set { if (value == _firstName) return; _firstName = value; OnPropertyChanged(); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
äœã®åé¡ïŒ ã¯ãããããã ãŸããã»ãã¿ãŒã®3è¡ãããã¯åé¡ã§ã¯ãããŸããã äžè¬ã«ãç§ã¯éåžžãèªåããããã£ãèšè¿°ãã身äœã®åããæå°éã«æãããªãŸã«ãã䜿çšããŠèªåãªãã¡ã¯ã¿ãªã³ã°ãè¡ããŸãã
ãããããŸã åé¡ããããŸãã FirstNameãå€æŽãããšãã«FullNameããããã£ãåæããå¿ èŠãããå Žåã¯ã©ããªããŸããïŒ ããã«ã¯2ã€ã®ãªãã·ã§ã³ããããŸããããã¯èšç®ããããã£ã§ãããå€æŽã«é¢ããã€ãã³ããçæããå¿ èŠãããããFirstNameãšåæ§ã«å®è£ ããå¿ èŠããããå€æŽããå¿ èŠããããŸãã æåã®ããŒãžã§ã³ã§ã¯ãFirstNameããããã£ã»ãã¿ãŒã¯å¿ èŠãªéç¥ãçæããŸãã
set { if (value == _firstName) return; _firstName = value; OnPropertyChanged(); OnPropertyChanged(nameof(FullName)); }
2çªç®ã§ã¯ãããããã£ã®æŽæ°ãåŒã³åºãããéç¥èªäœãçæãããŸãã
set { if (value == _firstName) return; _firstName = value; OnPropertyChanged(); UpdateFullName(); } private void UpdateFullName() { FullName = $"{FirstName} {LastName}"; }
ãŸã æ¯èŒçåçŽã«èŠããŸãããããã¯å°çãžã®éã§ãã LastNameããããFullNameãå€æŽããå¿ èŠããããŸãã 次ã«ãå ¥åããååã§æ€çŽ¢ãåºå®ãããšããã¹ãŠãããã«è€éã«ãªããŸãã ãããŠãå¥ã®ãå¥ã®...ãããŠãã³ãŒãã®é£ç¶çãªçæã€ãã³ãã§ãå€ãã®ã¢ã¯ã·ã§ã³ãã»ãã¿ãŒããèµ·åãããèãããããã¹ãŠã®å®è¡ãã¹ãèæ ®ãããŠããªãããäœããåŒã³åºãããŠããªããšããäºå®ã®ããã«ããã€ãã®ãšã©ãŒãçºçããç¶æ³ã«ãããŸããã®é åºãããã³ä»ã®æªå€¢ã
ãããŠäžè¬çã«ããªãFirstNameããããã£ã¯FullNameãã©ããã«ããããšããããŠååã§æ€çŽ¢ãå®è¡ããå¿ èŠãããããšãç¥ã£ãŠããã®ã§ããããïŒ ããã¯åœŒã®é¢å¿äºã§ã¯ãããŸããã ãããå€æŽããŠå ±åããå¿ èŠããããŸãã ã¯ããããªãã¯ãã®æ¹æ³ã§ãããè¡ãããšãã§ããããªãèªèº«ã®PropertyChangedã€ãã³ãã«åºå·ããŠè¿œå ã®ã¢ã¯ã·ã§ã³ãåŒã³åºãããšãã§ããŸãããããã«ã¯ããŸãåã³ã¯ãããŸãã-æã§ãè¡ã«æ¥ãå€æŽãããããããã£ã®ååã§ãããã®ã€ãã³ããå解ããŠãã ãã
ãããŠãåé ã«ç€ºããåçŽãªå®è£ ã¯ããããããå§ããŸããã»ãšãã©åãã³ãŒãã§ããŸã èªãŸãªããŠã¯ãªããŸããã
ReactiveUIã¯äœãæäŸããŸããïŒ
宣èšçã§äŸåé¢ä¿ãæŽçããŸãã
Nugetããã€ã³ã¹ããŒã«ããŸãã ãreactiveuiããæ¢ããŠããŸããçŸåšã®ããŒãžã§ã³6.5.0ã眮ããŸãã ããã§ã¯ãå©çšå¯èœãªæŽæ°ããã°ã©ã ã®ãªã¹ãã«ç§»åããããã«è¡šç€ºãããSplatãææ°ããŒãžã§ã³ïŒçŸåšã¯1.6.2ïŒã«æŽæ°ããŸãããã ããããªããã°ãããæç¹ã§ããã¹ãŠãèœã¡ãŸããã
ãã¬ãŒã ã¯ãŒã¯ãã€ã³ã¹ããŒã«ããã®ã§ãæåã®äŸãå°ãæ¹åããŠã¿ãŸãããã ãŸããReactiveObjectããç¶æ¿ããããããã£ã»ãã¿ãŒãæžãæããŸãã
public class PersonViewModel : ReactiveObject { private string _firstName; public string FirstName { get { return _firstName; } set { this.RaiseAndSetIfChanged(ref _firstName, value); UpdateFullName(); } } private string _lastName; public string LastName { get { return _lastName; } set { this.RaiseAndSetIfChanged(ref _lastName, value); UpdateFullName(); } } private string _fullName; public string FullName { get { return _fullName; } private set { this.RaiseAndSetIfChanged(ref _fullName, value); } } private void UpdateFullName() { FullName = $"{FirstName} {LastName}"; } }
ãŸã åããªãã ãã®ãããªRaiseAndSetIfChangedã¯ãæã§æžãããšãã§ããŸãã ããããReactiveObjectãINPCã ãã§ãªãå®è£ ããŠããããšãããã«èšã£ãŠãã䟡å€ããããŸãã
ããã§ã¯ãç¹ã«INotifyPropertyChangedãINotifyPropertyChangingãããã³ããã€ãã®3ã€ã®IObservable <>ã®å®è£ ã確èªããŸãã
ãªã¢ã¯ãã£ãã¢ãã«ã®è©³çŽ°ãèªã
ããã§ã¯ããããã©ã®ãããªIObservableã§ãããã«ã€ããŠããã€ãã®èšèãèšã䟡å€ããããŸãã ãããã¯ããã·ã¥ããŒã¹ã®éç¥ãããã€ããŒã§ãã åçã¯éåžžã«åçŽã§ãïŒå€å žçãªã¢ãã«ïŒãã«ããŒã¹ïŒã§ã¯ãããŒã¿ãããã€ããŒã«å¯ŸããŠå®è¡ããæŽæ°ã®ããã«ããŒãªã³ã°ããŸãã äºåŸå¯Ÿå¿-ãã®ãããªéç¥ãã£ã³ãã«ã«ç»é²ãã調æ»ã«ã€ããŠå¿é ããå¿ èŠã¯ãããŸããããã¹ãŠã®æŽæ°ã¯ç§ãã¡èªèº«ã«å±ããŸãïŒ
public interface IObservable<out T> { IDisposable Subscribe(IObserver<T> observer); }
IObserver <>ãšããŠåäœããŸã-ãªãã¶ãŒããŒïŒ
public interface IObserver<in T> { void OnNext(T value); void OnError(Exception error); void OnCompleted(); }
OnNextã¯ã次ã®éç¥ã衚瀺ããããšãã«åŒã³åºãããŸãã OnError-ãšã©ãŒãçºçããå Žåã OnCompleted-éç¥ãçµäºãããšãã
æ°ããéç¥ã¯ãã€ã§ãç»é²è§£é€ã§ããŸãããã®ãããSubscribeã¡ãœããã¯IDisposableãè¿ããŸãã DisposeãåŒã³åºããŸã-æ°ããéç¥ã¯ãããŸããã
ããã§ãChangedããµãã¹ã¯ã©ã€ãããŠFirstNameãå€æŽãããšãOnNextã¡ãœãããåŒã³åºããããã©ã¡ãŒã¿ãŒã¯ã€ãã³ãPropertyChangedãšåãæ å ±ïŒã€ãŸããéä¿¡è ãšããããã£ã®ååãžã®åç §ïŒãæã¡ãŸãã
ãŸããããã§ã¯å€ãã®ã¡ãœãããèªç±ã«äœ¿çšã§ããŸããããã®äžéšã¯LINQããã®ãã®ã§ãã ãã§ã«è©Šãããã®ãéžæããŠãã ããã ããã«äœãã§ããŸããïŒ Whereã䜿çšããŠéç¥ã®ã¹ããªãŒã ããã£ã«ã¿ãŒåŠçããDistinctç¹°ãè¿ãéç¥ãŸãã¯DistinctUntilChangedãäœæããŠåãé£ç¶éç¥ãåé¿ããTakeãSkipããã³ãã®ä»ã®LINQã¡ãœããã䜿çšããŸãã
Rxã®äœ¿çšäŸãèŠãŠã¿ãŸããã
var observable = Enumerable.Range(1, 4).ToObservable(); observable.Subscribe(Observer.Create<int>( i => Console.WriteLine(i), e => Console.WriteLine(e), () => Console.WriteLine("Taking numbers: complete") )); //1 //2 //3 //4 //Taking numbers: complete observable.Select(i => i*i).Subscribe(Observer.Create<int>( i => Console.WriteLine(i), e => Console.WriteLine(e), () => Console.WriteLine("Taking squares: complete") )); //1 //4 //9 //16 //Taking squares: complete observable.Take(2).Subscribe(Observer.Create<int>( i => Console.WriteLine(i), e => Console.WriteLine(e), () => Console.WriteLine("Taking two items: complete") )); //1 //2 //Taking two items: complete observable.Where(i => i % 2 != 0).Subscribe(Observer.Create<int>( i => Console.WriteLine(i), e => Console.WriteLine(e), () => Console.WriteLine("Taking odd numbers: complete") )); //1 //3 //Taking odd numbers: complete
ããã§ãããããã¹ãŠã®éç¥ãæéå ã«ç§»åããŠãäœãæ©èœãããã確èªã§ããŸãã
ããªãç°¡åã«å€æããŸããããä»ã®ãšããããã§ååã ãšæããŸãã 詳现ã«ã€ããŠã¯ãããšãã°hereãŸãã¯hereãåç §ããŠãã ãã ã
ReactiveUIã䜿çšããŠããããã£ããã€ã³ããã
åé¡ã®ããã³ãŒãã®æ¹åã«æ»ããŸãããã äŸåé¢ä¿ãæŽçããŸãããïŒ
public class PersonViewModel : ReactiveObject { private string _firstName; public string FirstName { get { return _firstName; } set { this.RaiseAndSetIfChanged(ref _firstName, value); } } private string _lastName; public string LastName { get { return _lastName; } set { this.RaiseAndSetIfChanged(ref _lastName, value); } } private string _fullName; public string FullName { get { return _fullName; } private set { this.RaiseAndSetIfChanged(ref _fullName, value); } } public PersonViewModel(string firstName, string lastName) { _firstName = firstName; _lastName = lastName; this.WhenAnyValue(vm => vm.FirstName, vm => vm.LastName).Subscribe(_ => UpdateFullName()); } private void UpdateFullName() { FullName = $"{FirstName} {LastName}"; } }
èŠãŠãããããã£ã«ã¯äœåãªãã®ã¯å«ãŸããŠããŸããããã¹ãŠã®äŸåé¢ä¿ã¯1ã€ã®å Žæãã€ãŸãã³ã³ã¹ãã©ã¯ã¿ãŒã§èšè¿°ãããŠããŸãã ããã§ã¯ãFirstNameãšLastNameã®å€æŽããµãã¹ã¯ã©ã€ãããäœããå€æŽããããšãã«UpdateFullNameïŒïŒãåŒã³åºããŸãã ãšããã§ã次ã®ããšãã§ããŸãããå°ãç°ãªããŸãã
public PersonViewModel(...) { ... this.WhenAnyValue(vm => vm.FirstName, vm => vm.LastName).Subscribe(t => UpdateFullName(t)); } private void UpdateFullName(Tuple<string, string> tuple) { FullName = $"{tuple.Item1} {tuple.Item2}"; }
éç¥ãã©ã¡ãŒã¿ãŒã¯ãçŸåšã®ããããã£å€ãå«ãã¿ãã«ã§ãã ãã«ããŒã ãæŽæ°ããã¡ãœããã«ããããæž¡ãããšãã§ããŸãã æŽæ°æ¹æ³ã¯éåžžã次ã®ããã«åé€ããŠå®è¡ã§ããŸããã
this.WhenAnyValue(vm => vm.FirstName, vm => vm.LastName).Subscribe(t => { FullName = $"{t.Item1} {t.Item2}"; });
ããäžåºŠFullNameãèŠãŠã¿ãŸãããã
private string _fullName; public string FullName { get { return _fullName; } private set { this.RaiseAndSetIfChanged(ref _fullName, value); } }
å®éã«ã¯ååã®äžéšã«å®å šã«äŸåããèªã¿åãå°çšã§ããå¿ èŠãããã®ã«ãããããå¯å€ããããã£ãå¿ èŠãªã®ã¯ãªãã§ããïŒ ãããä¿®æ£ããŸãããïŒ
private readonly ObservableAsPropertyHelper<string> _fullName; public string FullName => _fullName.Value; public PersonViewModel(...) { ... _fullName = this.WhenAnyValue(vm => vm.FirstName, vm => vm.LastName) .Select(t => $"{t.Item1} {t.Item2}") .ToProperty(this, vm => vm.FullName); }
ObservableAsPropertyHelper <>ã¯ãåºåããããã£ã®å®è£ ã«åœ¹ç«ã¡ãŸãã å éšã¯IObservableã§ãããããã£ã¯èªã¿åãå°çšã«ãªããŸãããå€æŽãè¡ããããšéç¥ãçæãããŸãã
ã¡ãªã¿ã«ãLINQããæ¥ããã®ã«å ããŠãIObservable <>ã«ã¯ãThrottleãªã©ã®ä»ã®èå³æ·±ãã¡ãœããããããŸãã
_fullName = this.WhenAnyValue(vm => vm.FirstName, vm => vm.LastName) .Select(t => $"{t.Item1} {t.Item2}") .Throttle(TimeSpan.FromSeconds(1)) .ToProperty(this, vm => vm.FullName);
éç¥ã¯ããã§ç Žæ£ããã1ç§ä»¥å ã«ä»¥äžãç¶ããŸãã ã€ãŸãããŠãŒã¶ãŒãååå ¥åãã£ãŒã«ãã«äœããå ¥åããéããFullNameã¯å€æŽãããŸããã 圌ãå°ãªããšã1ç§éåæ¢ãããšãæ°åãæŽæ°ãããŸãã
çµæ
æçµã³ãŒã
using System.Reactive.Linq; namespace ReactiveUI.Guide.ViewModel { public class PersonViewModel : ReactiveObject { private string _firstName; public string FirstName { get { return _firstName; } set { this.RaiseAndSetIfChanged(ref _firstName, value); } } private string _lastName; public string LastName { get { return _lastName; } set { this.RaiseAndSetIfChanged(ref _lastName, value); } } private readonly ObservableAsPropertyHelper<string> _fullName; public string FullName => _fullName.Value; public PersonViewModel(string firstName, string lastName) { _firstName = firstName; _lastName = lastName; _fullName = this.WhenAnyValue(vm => vm.FirstName, vm => vm.LastName) .Select(t => $"{t.Item1} {t.Item2}") .ToProperty(this, vm => vm.FullName); } } }
ããããã£éã®é¢ä¿ã1ã€ã®å Žæã§å®£èšçã«èšè¿°ãããŠããViewModelãååŸããŸããã ããã¯ç§ã«ãšã£ãŠçŽ æŽãããæ©äŒã®ããã«æããŸãããããã®ããããã£ãŸãã¯ãããã®ããããã£ãå€æŽããããšãã«äœãèµ·ããããç解ããããšããŠãã³ãŒãå šäœãã·ã£ãã«ããå¿ èŠã¯ãããŸããã å¯äœçšã¯ãããŸãã-ãã¹ãŠãæããã§ãã ãã¡ãããããããã¹ãŠã®æäœã®çµæã¯ããã©ãŒãã³ã¹ã®äœäžã§ãã æ·±å»ãªåé¡ã§ã¯ãããŸããããããã¯ã·ã¹ãã ã®ã³ã¢ã§ã¯ãªããå¯èœãªéãçç£çã§ããå¿ èŠããããŸãããViewModelã¬ã€ã€ãŒã§ãã
誰ãããã®èšäºãæçšã§èå³æ·±ããã®ã«ãªãããããžã§ã¯ãã§èª¬æãããŠãããã¯ãããžãŒã䜿çšããŠã¿ãŠãã ããã å°æ¥çã«ã¯ããªã¢ã¯ãã£ãã³ã¬ã¯ã·ã§ã³ãã³ãã³ããªã©ã«ã€ããŠèª¬æããViewã¬ã€ã€ãŒãšViewModelã¬ã€ã€ãŒéã®çžäºäœçšãã«ãŒãã£ã³ã°ããŠãŒã¶ãŒã®çžäºäœçšã瀺ãããè€éãªäŸã«åãæãããããšèããŠããŸãã
ãæž èŽããããšãããããŸããïŒ