diff --git a/samples/getting-started/ReactiveDemo.sln b/samples/getting-started/ReactiveDemo.sln new file mode 100644 index 0000000000..a26068cff4 --- /dev/null +++ b/samples/getting-started/ReactiveDemo.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28010.2003 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveDemo", "ReactiveDemo\ReactiveDemo.csproj", "{996C151B-7AAF-4C5C-A786-52DF9C251A73}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {996C151B-7AAF-4C5C-A786-52DF9C251A73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {996C151B-7AAF-4C5C-A786-52DF9C251A73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {996C151B-7AAF-4C5C-A786-52DF9C251A73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {996C151B-7AAF-4C5C-A786-52DF9C251A73}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D4A4A79B-20F4-41EC-BADF-141BA15D261F} + EndGlobalSection +EndGlobal diff --git a/samples/getting-started/ReactiveDemo/App.config b/samples/getting-started/ReactiveDemo/App.config new file mode 100644 index 0000000000..56efbc7b5f --- /dev/null +++ b/samples/getting-started/ReactiveDemo/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/getting-started/ReactiveDemo/App.xaml b/samples/getting-started/ReactiveDemo/App.xaml new file mode 100644 index 0000000000..25a503fd0e --- /dev/null +++ b/samples/getting-started/ReactiveDemo/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/samples/getting-started/ReactiveDemo/App.xaml.cs b/samples/getting-started/ReactiveDemo/App.xaml.cs new file mode 100644 index 0000000000..3a2731400b --- /dev/null +++ b/samples/getting-started/ReactiveDemo/App.xaml.cs @@ -0,0 +1,18 @@ +using ReactiveUI; +using Splat; +using System.Reflection; +using System.Windows; + +namespace ReactiveDemo +{ + public partial class App : Application + { + public App() + { + // A helper method that will register all classes that derive off IViewFor + // into our dependency injection container. ReactiveUI uses Splat for it's + // dependency injection by default, but you can override this if you like. + Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetCallingAssembly()); + } + } +} diff --git a/samples/getting-started/ReactiveDemo/AppViewModel.cs b/samples/getting-started/ReactiveDemo/AppViewModel.cs new file mode 100644 index 0000000000..3989f31c29 --- /dev/null +++ b/samples/getting-started/ReactiveDemo/AppViewModel.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Reactive.Linq; +using System.Threading; +using System.Linq; +using ReactiveUI; +using NuGet.Protocol.Core.Types; +using NuGet.Protocol; +using NuGet.Configuration; + +namespace ReactiveDemo +{ + // AppViewModel is where we will describe the interaction of our application. + // We can describe the entire application in one class since it's very small now. + // Most ViewModels will derive off ReactiveObject, while most Model classes will + // most derive off INotifyPropertyChanged + public class AppViewModel : ReactiveObject + { + // In ReactiveUI, this is the syntax to declare a read-write property + // that will notify Observers, as well as WPF, that a property has + // changed. If we declared this as a normal property, we couldn't tell + // when it has changed! + private string _searchTerm; + public string SearchTerm + { + get => _searchTerm; + set => this.RaiseAndSetIfChanged(ref _searchTerm, value); + } + + // Here's the interesting part: In ReactiveUI, we can take IObservables + // and "pipe" them to a Property - whenever the Observable yields a new + // value, we will notify ReactiveObject that the property has changed. + // + // To do this, we have a class called ObservableAsPropertyHelper - this + // class subscribes to an Observable and stores a copy of the latest value. + // It also runs an action whenever the property changes, usually calling + // ReactiveObject's RaisePropertyChanged. + private readonly ObservableAsPropertyHelper> _searchResults; + public IEnumerable SearchResults => _searchResults.Value; + + // Here, we want to create a property to represent when the application + // is performing a search (i.e. when to show the "spinner" control that + // lets the user know that the app is busy). We also declare this property + // to be the result of an Observable (i.e. its value is derived from + // some other property) + private readonly ObservableAsPropertyHelper _isAvailable; + public bool IsAvailable => _isAvailable.Value; + + public AppViewModel() + { + // Creating our UI declaratively + // + // The Properties in this ViewModel are related to each other in different + // ways - with other frameworks, it is difficult to describe each relation + // succinctly; the code to implement "The UI spinner spins while the search + // is live" usually ends up spread out over several event handlers. + // + // However, with ReactiveUI, we can describe how properties are related in a + // very organized clear way. Let's describe the workflow of what the user does + // in this application, in the order they do it. + + // We're going to take a Property and turn it into an Observable here - this + // Observable will yield a value every time the Search term changes, which in + // the XAML, is connected to the TextBox. + // + // We're going to use the Throttle operator to ignore changes that happen too + // quickly, since we don't want to issue a search for each key pressed! We + // then pull the Value of the change, then filter out changes that are identical, + // as well as strings that are empty. + // + // We then do a SelectMany() which starts the task by converting Task> + // into IObservable>. If subsequent requests are made, the + // CancellationToken is called. We then ObservableOn the main thread, + // everything up until this point has been running on a separate thread due + // to the Throttle(). + // + // We then use a ObservableAsPropertyHelper and the ToProperty() method to allow + // us to have the latest results that we can expose through the property to the View. + _searchResults = this + .WhenAnyValue(x => x.SearchTerm) + .Throttle(TimeSpan.FromMilliseconds(800)) + .Select(term => term?.Trim()) + .DistinctUntilChanged() + .Where(term => !string.IsNullOrWhiteSpace(term)) + .SelectMany(SearchNuGetPackages) + .ObserveOn(RxApp.MainThreadScheduler) + .ToProperty(this, x => x.SearchResults); + + // We subscribe to the "ThrownExceptions" property of our OAPH, where ReactiveUI + // marshals any exceptions that are thrown in SearchNuGetPackages method. + // See the "Error Handling" section for more information about this. + _searchResults.ThrownExceptions.Subscribe(error => { /* Handle errors here */ }); + + // A helper method we can use for Visibility or Spinners to show if results are available. + // We get the latest value of the SearchResults and make sure it's not null. + _isAvailable = this + .WhenAnyValue(x => x.SearchResults) + .Select(searchResults => searchResults != null) + .ToProperty(this, x => x.IsAvailable); + } + + // Here we search NuGet packages using the NuGet.Client library. Ideally, we should + // extract such code into a separate service, say, INuGetSearchService, but let's + // try to avoid overcomplicating things at this time. + private async Task> SearchNuGetPackages( + string term, CancellationToken token) + { + var providers = new List>(); + providers.AddRange(Repository.Provider.GetCoreV3()); // Add v3 API support + var packageSource = new PackageSource("https://api.nuget.org/v3/index.json"); + var sourceRepository = new SourceRepository(packageSource, providers); + + var filter = new SearchFilter(false); + var searchResource = await sourceRepository.GetResourceAsync(); + var searchMetadata = await searchResource.SearchAsync(term, filter, 0, 10, null, token); + return searchMetadata.Select(x => new NugetDetailsViewModel(x)); + } + } +} diff --git a/samples/getting-started/ReactiveDemo/MainWindow.xaml b/samples/getting-started/ReactiveDemo/MainWindow.xaml new file mode 100644 index 0000000000..14a1edd4fb --- /dev/null +++ b/samples/getting-started/ReactiveDemo/MainWindow.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/getting-started/ReactiveDemo/MainWindow.xaml.cs b/samples/getting-started/ReactiveDemo/MainWindow.xaml.cs new file mode 100644 index 0000000000..f1c1b5d6ae --- /dev/null +++ b/samples/getting-started/ReactiveDemo/MainWindow.xaml.cs @@ -0,0 +1,65 @@ +using System.Reactive.Disposables; +using System.Windows; +using ReactiveUI; + +namespace ReactiveDemo +{ + public partial class MainWindow : IViewFor + { + // Using a DependencyProperty as the backing store for ViewModel. + // This enables animation, styling, binding, etc. + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register("ViewModel", + typeof(AppViewModel), typeof(MainWindow), + new PropertyMetadata(null)); + + public MainWindow() + { + InitializeComponent(); + ViewModel = new AppViewModel(); + + // We create our bindings here. These are the code behind bindings which allow + // type safety. The bindings will only become active when the Window is being shown. + // We register our subscription in our disposableRegistration, this will cause + // the binding subscription to become inactive when the Window is closed. + // The disposableRegistration is a CompositeDisposable which is a container of + // other Disposables. We use the DisposeWith() extension method which simply adds + // the subscription disposable to the CompositeDisposable. + this.WhenActivated(disposableRegistration => + { + // Notice we don't have to provide a converter, on WPF a global converter is + // registered which knows how to convert a boolean into visibility. + this.OneWayBind(ViewModel, + viewModel => viewModel.IsAvailable, + view => view.searchResultsListBox.Visibility) + .DisposeWith(disposableRegistration); + + this.OneWayBind(ViewModel, + viewModel => viewModel.SearchResults, + view => view.searchResultsListBox.ItemsSource) + .DisposeWith(disposableRegistration); + + this.Bind(ViewModel, + viewModel => viewModel.SearchTerm, + view => view.searchTextBox.Text) + .DisposeWith(disposableRegistration); + }); + } + + // Our main view model instance. + public AppViewModel ViewModel + { + get => (AppViewModel)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + // This is required by the interface IViewFor, you always just set it to use the + // main ViewModel property. Note on XAML based platforms we have a control called + // ReactiveUserControl that abstracts this. + object IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (AppViewModel)value; + } + } +} diff --git a/samples/getting-started/ReactiveDemo/NugetDetailsView.xaml b/samples/getting-started/ReactiveDemo/NugetDetailsView.xaml new file mode 100644 index 0000000000..e6146c003d --- /dev/null +++ b/samples/getting-started/ReactiveDemo/NugetDetailsView.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + Open + + + \ No newline at end of file diff --git a/samples/getting-started/ReactiveDemo/NugetDetailsView.xaml.cs b/samples/getting-started/ReactiveDemo/NugetDetailsView.xaml.cs new file mode 100644 index 0000000000..70a3e7a644 --- /dev/null +++ b/samples/getting-started/ReactiveDemo/NugetDetailsView.xaml.cs @@ -0,0 +1,43 @@ +using ReactiveUI; +using System.Reactive.Disposables; +using System.Windows.Media.Imaging; + +namespace ReactiveDemo +{ + // The class derives off ReactiveUserControl which contains the ViewModel property. + // In our MainWindow when we register the ListBox with the collection of + // NugetDetailsViewModels if no ItemTemplate has been declared it will search for + // a class derived off IViewFor and show that for the item. + public partial class NugetDetailsView : ReactiveUserControl + { + public NugetDetailsView() + { + InitializeComponent(); + this.WhenActivated(disposableRegistration => + { + // Our 4th parameter we convert from Url into a BitmapImage. + // This is an easy way of doing value conversion using ReactiveUI binding. + this.OneWayBind(ViewModel, + viewModel => viewModel.IconUrl, + view => view.iconImage.Source, + url => url == null ? null : new BitmapImage(url)) + .DisposeWith(disposableRegistration); + + this.OneWayBind(ViewModel, + viewModel => viewModel.Title, + view => view.titleRun.Text) + .DisposeWith(disposableRegistration); + + this.OneWayBind(ViewModel, + viewModel => viewModel.Description, + view => view.descriptionRun.Text) + .DisposeWith(disposableRegistration); + + this.BindCommand(ViewModel, + viewModel => viewModel.OpenPage, + view => view.openButton) + .DisposeWith(disposableRegistration); + }); + } + } +} diff --git a/samples/getting-started/ReactiveDemo/NugetDetailsViewModel.cs b/samples/getting-started/ReactiveDemo/NugetDetailsViewModel.cs new file mode 100644 index 0000000000..b87b9bb7b2 --- /dev/null +++ b/samples/getting-started/ReactiveDemo/NugetDetailsViewModel.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics; +using System.Reactive; +using NuGet.Protocol.Core.Types; +using ReactiveUI; + +namespace ReactiveDemo +{ + // This class wraps out NuGet model object into a ViewModel and allows + // us to have a ReactiveCommand to open the NuGet package URL. + public class NugetDetailsViewModel : ReactiveObject + { + private readonly IPackageSearchMetadata _metadata; + private readonly Uri _defaultUrl; + + public NugetDetailsViewModel(IPackageSearchMetadata metadata) + { + _metadata = metadata; + _defaultUrl = new Uri("https://git.io/fAlfh"); + OpenPage = ReactiveCommand.Create(() => { Process.Start(ProjectUrl.ToString()); }); + } + + public Uri IconUrl => _metadata.IconUrl ?? _defaultUrl; + public string Description => _metadata.Description; + public Uri ProjectUrl => _metadata.ProjectUrl; + public string Title => _metadata.Title; + + // ReactiveCommand allows us to execute logic without exposing any of the + // implementation details with the View. The generic parameters are the + // input into the command and it's output. In our case we don't have any + // input or output so we use Unit which in Reactive speak means a void type. + public ReactiveCommand OpenPage { get; } + } +} diff --git a/samples/getting-started/ReactiveDemo/Properties/AssemblyInfo.cs b/samples/getting-started/ReactiveDemo/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..9b8288b7c9 --- /dev/null +++ b/samples/getting-started/ReactiveDemo/Properties/AssemblyInfo.cs @@ -0,0 +1,24 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +[assembly: AssemblyTitle("ReactiveDemo")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ReactiveDemo")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, + ResourceDictionaryLocation.SourceAssembly +)] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/getting-started/ReactiveDemo/ReactiveDemo.csproj b/samples/getting-started/ReactiveDemo/ReactiveDemo.csproj new file mode 100644 index 0000000000..f6bc442c29 --- /dev/null +++ b/samples/getting-started/ReactiveDemo/ReactiveDemo.csproj @@ -0,0 +1,78 @@ + + + + + Debug + AnyCPU + {996C151B-7AAF-4C5C-A786-52DF9C251A73} + WinExe + ReactiveDemo + ReactiveDemo + v4.7.2 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + + MainWindow.xaml + Code + + + Designer + MSBuild:Compile + + + + + NugetDetailsView.xaml + + + + Code + + + + + + + + + + + + \ No newline at end of file