Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions samples/getting-started/ReactiveDemo.sln
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions samples/getting-started/ReactiveDemo/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>
9 changes: 9 additions & 0 deletions samples/getting-started/ReactiveDemo/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Application x:Class="ReactiveDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ReactiveDemo"
StartupUri="MainWindow.xaml">
<Application.Resources>

</Application.Resources>
</Application>
18 changes: 18 additions & 0 deletions samples/getting-started/ReactiveDemo/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
120 changes: 120 additions & 0 deletions samples/getting-started/ReactiveDemo/AppViewModel.cs
Original file line number Diff line number Diff line change
@@ -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<IEnumerable<NugetDetailsViewModel>> _searchResults;
public IEnumerable<NugetDetailsViewModel> 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<bool> _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<IEnumerable<T>>
// into IObservable<IEnumerable<T>>. 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<IEnumerable<NugetDetailsViewModel>> SearchNuGetPackages(
string term, CancellationToken token)
{
var providers = new List<Lazy<INuGetResourceProvider>>();
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<PackageSearchResource>();
var searchMetadata = await searchResource.SearchAsync(term, filter, 0, 10, null, token);
return searchMetadata.Select(x => new NugetDetailsViewModel(x));
}
}
}
24 changes: 24 additions & 0 deletions samples/getting-started/ReactiveDemo/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Window x:Class="ReactiveDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="NuGet Browser"
mc:Ignorable="d" Height="450" Width="800">
<Grid Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock FontSize="16" FontWeight="SemiBold"
VerticalAlignment="Center" Text="Search for: "/>
<TextBox Grid.Column="1" Margin="6 0 0 0" x:Name="searchTextBox" />
<ListBox x:Name="searchResultsListBox" Grid.ColumnSpan="3"
Grid.Row="1" Margin="0,6,0,0" HorizontalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" />
</Grid>
</Window>
65 changes: 65 additions & 0 deletions samples/getting-started/ReactiveDemo/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Reactive.Disposables;
using System.Windows;
using ReactiveUI;

namespace ReactiveDemo
{
public partial class MainWindow : IViewFor<AppViewModel>
{
// 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;
}
}
}
24 changes: 24 additions & 0 deletions samples/getting-started/ReactiveDemo/NugetDetailsView.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<reactiveui:ReactiveUserControl
x:Class="ReactiveDemo.NugetDetailsView"
xmlns:reactiveDemo="clr-namespace:ReactiveDemo"
x:TypeArguments="reactiveDemo:NugetDetailsViewModel"
xmlns:reactiveui="http://reactiveui.net"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image x:Name="iconImage" Margin="6" Width="64" Height="64"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" TextWrapping="WrapWithOverflow"
Margin="6" VerticalAlignment="Center">
<Run FontSize="14" FontWeight="SemiBold" x:Name="titleRun"/>
<LineBreak />
<Run FontSize="12" x:Name="descriptionRun"/>
<LineBreak />
<Hyperlink x:Name="openButton">Open</Hyperlink>
</TextBlock>
</Grid>
</reactiveui:ReactiveUserControl>
43 changes: 43 additions & 0 deletions samples/getting-started/ReactiveDemo/NugetDetailsView.xaml.cs
Original file line number Diff line number Diff line change
@@ -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<NugetDetailsViewModel> and show that for the item.
public partial class NugetDetailsView : ReactiveUserControl<NugetDetailsViewModel>
{
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);
});
}
}
}
34 changes: 34 additions & 0 deletions samples/getting-started/ReactiveDemo/NugetDetailsViewModel.cs
Original file line number Diff line number Diff line change
@@ -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<Unit, Unit> OpenPage { get; }
}
}
Loading