Add page routing and support various detail views (#1745)

samples: Add xamarin page routing and support various detail views (#1745)
This commit is contained in:
Colt Bauman 2018-09-09 07:16:48 +09:00 коммит произвёл Rodney Littles II
Родитель fdec8e19b5
Коммит 0e8b0994a6
29 изменённых файлов: 421 добавлений и 179 удалений

Просмотреть файл

@ -49,7 +49,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI" Version="*" />
<PackageReference Include="ReactiveUI" Version="8.7.2" />
<PackageReference Include="Xamarin.Forms" Version="3.1.0.697729" />
<PackageReference Include="Xamarin.Android.Support.Design" Version="27.0.2.1" />
<PackageReference Include="Xamarin.Android.Support.v7.AppCompat" Version="27.0.2.1" />

Просмотреть файл

@ -13,9 +13,9 @@ namespace MasterDetail
{
InitializeComponent();
Locator.CurrentMutable.Register(() => new CustomCell(), typeof(IViewFor<CustomCellViewModel>));
var bootstrapper = new AppBootstrapper();
MainPage = new MainPage();
MainPage = new MainPage(bootstrapper.CreateMainViewModel());
}
protected override void OnStart()

Просмотреть файл

@ -0,0 +1,45 @@
using System;
using ReactiveUI;
using Splat;
namespace MasterDetail
{
public class AppBootstrapper
{
public AppBootstrapper()
{
RegisterViews();
RegisterViewModels();
}
private void RegisterViews()
{
Locator.CurrentMutable.Register(() => new DummyPage(), typeof(IViewFor<DummyViewModel>));
Locator.CurrentMutable.Register(() => new MasterCell(), typeof(IViewFor<MasterCellViewModel>));
// Detail pages
Locator.CurrentMutable.Register(() => new NavigablePage(), typeof(IViewFor<NavigableViewModel>));
Locator.CurrentMutable.Register(() => new NumberStreamPage(), typeof(IViewFor<NumberStreamViewModel>));
Locator.CurrentMutable.Register(() => new LetterStreamPage(), typeof(IViewFor<LetterStreamViewModel>));
}
public MainViewModel CreateMainViewModel()
{
// In a typical routing example the IScreen implementation would be this bootstrapper class.
// However, a MasterDetailPage is designed to at the root. So, we assign the master-detail
// view model to play the part of IScreen, instead.
var viewModel = new MainViewModel();
return viewModel;
}
private void RegisterViewModels()
{
// Here, we use contracts to distinguish which routable view model we want to instantiate.
// This helps us avoid a manual cast to IRoutableViewModel when calling Router.Navigate.Execute(...)
Locator.CurrentMutable.Register(() => new NavigableViewModel(), typeof(IRoutableViewModel), typeof(NavigableViewModel).FullName);
Locator.CurrentMutable.Register(() => new NumberStreamViewModel(), typeof(IRoutableViewModel), typeof(NumberStreamViewModel).FullName);
Locator.CurrentMutable.Register(() => new LetterStreamViewModel(), typeof(IRoutableViewModel), typeof(LetterStreamViewModel).FullName);
}
}
}

Просмотреть файл

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rxui:ReactiveViewCell x:Class="MasterDetail.CustomCell"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:local="clr-namespace:MasterDetail"
x:TypeArguments="local:CustomCellViewModel">
<Grid Padding="5,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image x:Name="IconImage" />
<Label x:Name="TitleLabel" Grid.Column="1" />
</Grid>
</rxui:ReactiveViewCell>

Просмотреть файл

@ -1,26 +0,0 @@
using System.Reactive.Disposables;
using ReactiveUI;
using ReactiveUI.XamForms;
using Xamarin.Forms;
namespace MasterDetail
{
public partial class CustomCell : ReactiveViewCell<CustomCellViewModel>
{
public CustomCell()
{
InitializeComponent();
this.WhenActivated(
disposables =>
{
this
.OneWayBind(ViewModel, vm => vm.Title, v => v.TitleLabel.Text)
.DisposeWith(disposables);
this
.OneWayBind(ViewModel, vm => vm.IconSource, v => v.IconImage.Source, x => ImageSource.FromFile(x))
.DisposeWith(disposables);
});
}
}
}

Просмотреть файл

@ -1,18 +0,0 @@
using ReactiveUI;
namespace MasterDetail
{
public class CustomCellViewModel : ReactiveObject
{
public CustomCellViewModel(MyModel model)
{
Model = model;
}
public MyModel Model { get; }
public string Title => Model.Title;
public string IconSource => Model.IconSource;
}
}

Просмотреть файл

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactiveContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:local="clr-namespace:MasterDetail"
x:Class="MasterDetail.LetterStreamPage"
x:TypeArguments="local:LetterStreamViewModel">
<ContentPage.Content>
<StackLayout>
<Label
x:Name="LetterLabel"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
FontSize="36" />
</StackLayout>
</ContentPage.Content>
</rxui:ReactiveContentPage>

Просмотреть файл

@ -0,0 +1,26 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using ReactiveUI;
using ReactiveUI.XamForms;
using Xamarin.Forms.Xaml;
namespace MasterDetail
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class LetterStreamPage : ReactiveContentPage<LetterStreamViewModel>
{
public LetterStreamPage()
{
InitializeComponent();
this.WhenActivated(
disposables =>
{
this
.OneWayBind(ViewModel, vm => vm.CurrentLetter, v => v.LetterLabel.Text)
.DisposeWith(disposables);
});
}
}
}

Просмотреть файл

@ -0,0 +1,27 @@
using System;
using System.Reactive.Linq;
using ReactiveUI;
namespace MasterDetail
{
public class LetterStreamViewModel : ReactiveObject, IRoutableViewModel
{
private ObservableAsPropertyHelper<char> _currentLetter;
public LetterStreamViewModel()
{
_currentLetter = Observable
.Interval(TimeSpan.FromSeconds(1))
.Scan(64, (acc, current) => acc + 1)
.Select(x => (char)x)
.Take(26)
.ToProperty(this, x => x.CurrentLetter, scheduler: RxApp.MainThreadScheduler);
}
public char CurrentLetter => _currentLetter.Value;
public string UrlPathSegment => "Letter Stream Page";
public IScreen HostScreen { get; }
}
}

Просмотреть файл

@ -3,12 +3,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:local="clr-namespace:MasterDetail"
x:Class="MasterDetail.MyDetailPage"
x:TypeArguments="local:MyDetailViewModel">
x:Class="MasterDetail.NavigablePage"
x:TypeArguments="local:NavigableViewModel">
<ContentPage.Content>
<StackLayout>
<Label
x:Name="TitleLabel"
<Button
x:Name="NavigateButton"
Text="Navigate"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>

Просмотреть файл

@ -0,0 +1,25 @@
using System;
using System.Reactive.Disposables;
using ReactiveUI;
using ReactiveUI.XamForms;
using Xamarin.Forms.Xaml;
namespace MasterDetail
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class NavigablePage : ReactiveContentPage<NavigableViewModel>
{
public NavigablePage()
{
InitializeComponent();
this.WhenActivated(
disposables =>
{
this
.BindCommand(ViewModel, vm => vm.NavigateToDummyPage, v => v.NavigateButton)
.DisposeWith(disposables);
});
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using ReactiveUI;
using Splat;
namespace MasterDetail
{
public class NavigableViewModel : ReactiveObject, IRoutableViewModel
{
public NavigableViewModel(IScreen hostScreen = null)
{
HostScreen = hostScreen ?? Locator.Current.GetService<IScreen>();
NavigateToDummyPage = ReactiveCommand.CreateFromObservable(
() => HostScreen.Router.Navigate.Execute(new DummyViewModel()).Select(_ => Unit.Default));
}
public ReactiveCommand<Unit, Unit> NavigateToDummyPage { get; }
public string UrlPathSegment => "Navigable Page";
public IScreen HostScreen { get; }
}
}

Просмотреть файл

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactiveContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:local="clr-namespace:MasterDetail"
x:Class="MasterDetail.NumberStreamPage"
x:TypeArguments="local:NumberStreamViewModel">
<ContentPage.Content>
<StackLayout>
<Label
x:Name="NumberLabel"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand"
FontSize="36" />
</StackLayout>
</ContentPage.Content>
</rxui:ReactiveContentPage>

Просмотреть файл

@ -0,0 +1,29 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using ReactiveUI;
using ReactiveUI.XamForms;
using Xamarin.Forms.Xaml;
namespace MasterDetail
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class NumberStreamPage : ReactiveContentPage<NumberStreamViewModel>
{
public NumberStreamPage()
{
InitializeComponent();
this.WhenActivated(
disposables =>
{
this
.ViewModel
.NumberStream
.ObserveOn(RxApp.MainThreadScheduler)
.BindTo(this, x => x.NumberLabel.Text)
.DisposeWith(disposables);
});
}
}
}

Просмотреть файл

@ -0,0 +1,22 @@
using System;
using System.Reactive.Linq;
using ReactiveUI;
namespace MasterDetail
{
public class NumberStreamViewModel : ReactiveObject, IRoutableViewModel
{
public NumberStreamViewModel()
{
NumberStream = Observable
.Interval(TimeSpan.FromSeconds(1))
.Select(x => x.ToString());
}
public IObservable<string> NumberStream { get; }
public string UrlPathSegment => "Number Stream Page";
public IScreen HostScreen { get; }
}
}

Просмотреть файл

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactiveContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:local="clr-namespace:MasterDetail"
x:Class="MasterDetail.DummyPage"
x:TypeArguments="local:DummyViewModel">
<ContentPage.Content>
<StackLayout Spacing="10">
<Button
x:Name="NavigateButton"
Text="Navigate"
VerticalOptions="CenterAndExpand"
HorizontalOptions="Center" />
<Button
x:Name="BackButton"
Text="Navigate Back"
VerticalOptions="CenterAndExpand"
HorizontalOptions="Center" />
</StackLayout>
</ContentPage.Content>
</rxui:ReactiveContentPage>

Просмотреть файл

@ -0,0 +1,28 @@
using System;
using System.Reactive.Disposables;
using ReactiveUI;
using ReactiveUI.XamForms;
using Xamarin.Forms.Xaml;
namespace MasterDetail
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class DummyPage : ReactiveContentPage<DummyViewModel>
{
public DummyPage()
{
InitializeComponent();
this.WhenActivated(
disposables =>
{
this
.BindCommand(ViewModel, vm => vm.NavigateToDummyPage, v => v.NavigateButton)
.DisposeWith(disposables);
this
.BindCommand(ViewModel, vm => vm.NavigateBack, v => v.BackButton)
.DisposeWith(disposables);
});
}
}
}

Просмотреть файл

@ -0,0 +1,26 @@
using System.Reactive;
using System.Reactive.Linq;
using ReactiveUI;
using Splat;
namespace MasterDetail
{
public class DummyViewModel : ReactiveObject, IRoutableViewModel
{
public DummyViewModel(IScreen hostScreen = null)
{
HostScreen = hostScreen ?? Locator.Current.GetService<IScreen>();
NavigateToDummyPage = ReactiveCommand.CreateFromObservable(
() => HostScreen.Router.Navigate.Execute(new DummyViewModel()).Select(_ => Unit.Default));
}
public ReactiveCommand<Unit, Unit> NavigateToDummyPage { get; }
public ReactiveCommand<Unit, Unit> NavigateBack => HostScreen.Router.NavigateBack;
public string UrlPathSegment => "Dummy Page";
public IScreen HostScreen { get; }
}
}

Просмотреть файл

@ -4,8 +4,7 @@
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:local="clr-namespace:MasterDetail"
x:Class="MasterDetail.MainPage"
x:TypeArguments="local:MainViewModel"
NavigationPage.HasNavigationBar="False">
x:TypeArguments="local:MainViewModel">
<MasterDetailPage.Master>
<ContentPage Title="Master" Padding="0,40,0,0" Icon="hamburger.png">
@ -13,7 +12,7 @@
<ListView x:Name="MyListView" SeparatorVisibility="None">
<ListView.ItemTemplate>
<DataTemplate>
<local:CustomCell />
<local:MasterCell />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
@ -21,4 +20,8 @@
</ContentPage>
</MasterDetailPage.Master>
<MasterDetailPage.Detail>
<rxui:RoutedViewHost x:Name="ViewHost" />
</MasterDetailPage.Detail>
</rxui:ReactiveMasterDetailPage>

Просмотреть файл

@ -4,24 +4,22 @@ using System.Reactive.Disposables;
using System.Reactive.Linq;
using ReactiveUI;
using ReactiveUI.XamForms;
using Xamarin.Forms;
namespace MasterDetail
{
public partial class MainPage : ReactiveMasterDetailPage<MainViewModel>
{
public MainPage()
public MainPage(MainViewModel viewModel)
{
InitializeComponent();
ViewModel = viewModel;
ViewModel = new MainViewModel();
Detail = new NavigationPage(new MyDetailPage(ViewModel.Detail));
InitializeComponent();
this.WhenActivated(
disposables =>
{
this
.OneWayBind(ViewModel, vm => vm.MyList, v => v.MyListView.ItemsSource)
.OneWayBind(ViewModel, vm => vm.MenuItems, v => v.MyListView.ItemsSource)
.DisposeWith(disposables);
this
.Bind(ViewModel, vm => vm.Selected, v => v.MyListView.SelectedItem)
@ -30,9 +28,11 @@ namespace MasterDetail
.WhenAnyValue(x => x.ViewModel.Selected)
.Where(x => x != null)
.Subscribe(
model =>
_ =>
{
// Deselect the cell.
MyListView.SelectedItem = null;
// Hide the master panel.
IsPresented = false;
})
.DisposeWith(disposables);

Просмотреть файл

@ -1,45 +1,52 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using ReactiveUI;
using Splat;
namespace MasterDetail
{
public class MainViewModel : ReactiveObject
public class MainViewModel : ReactiveObject, IScreen
{
public MainViewModel()
{
var cellVms = GetData().Select(model => new CustomCellViewModel(model));
MyList = new ObservableCollection<CustomCellViewModel>(cellVms);
Router = new RoutingState();
Locator.CurrentMutable.RegisterConstant(this, typeof(IScreen));
Detail = new MyDetailViewModel();
Detail.Model = cellVms.First().Model;
MenuItems = GetMenuItems();
NavigateToMenuItem = ReactiveCommand.CreateFromObservable<IRoutableViewModel, Unit>(
routableVm => Router.NavigateAndReset.Execute(routableVm).Select(_ => Unit.Default));
this.WhenAnyValue(x => x.Selected)
.Where(x => x != null)
.Subscribe(cellVm => Detail.Model = cellVm.Model);
.StartWith(MenuItems.First())
.Select(x => Locator.Current.GetService<IRoutableViewModel>(x.TargetType.FullName))
.InvokeCommand(NavigateToMenuItem);
}
private CustomCellViewModel _selected;
public CustomCellViewModel Selected
private MasterCellViewModel _selected;
public MasterCellViewModel Selected
{
get => _selected;
set => this.RaiseAndSetIfChanged(ref _selected, value);
}
public MyDetailViewModel Detail { get; }
public ReactiveCommand<IRoutableViewModel, Unit> NavigateToMenuItem { get; }
public ObservableCollection<CustomCellViewModel> MyList { get; }
public IEnumerable<MasterCellViewModel> MenuItems { get; }
private IEnumerable<MyModel> GetData()
public RoutingState Router { get; }
private IEnumerable<MasterCellViewModel> GetMenuItems()
{
return new[]
{
new MyModel { Title = "Contacts", IconSource = "contacts.png" },
new MyModel { Title = "Reminders", IconSource = "reminders.png" },
new MyModel { Title = "TodoList", IconSource = "todo.png" },
new MasterCellViewModel { Title = "Navigable Page", IconSource = "contacts.png", TargetType = typeof(NavigableViewModel) },
new MasterCellViewModel { Title = "Number Stream Page", IconSource = "reminders.png", TargetType = typeof(NumberStreamViewModel) },
new MasterCellViewModel { Title = "Letter Stream Page", IconSource = "todo.png", TargetType = typeof(LetterStreamViewModel) },
};
}
}

Просмотреть файл

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<rxui:ReactiveViewCell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:local="clr-namespace:MasterDetail"
x:Class="MasterDetail.MasterCell"
x:TypeArguments="local:MasterCellViewModel">
<Grid Padding="5,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image x:Name="IconImage" />
<Label x:Name="TitleLabel" Grid.Column="1" />
</Grid>
</rxui:ReactiveViewCell>

Просмотреть файл

@ -0,0 +1,35 @@
using System;
using System.Reactive.Linq;
using ReactiveUI;
using ReactiveUI.XamForms;
namespace MasterDetail
{
public partial class MasterCell : ReactiveViewCell<MasterCellViewModel>
{
public MasterCell()
{
InitializeComponent();
// Disposal of this subsciption is *not* necessary because we're simply monitoring
// the property (ViewModel) on the view itself, so the subscription is attaching to
// PropertyChanged on this view. This means the view has a reference to itself and
// thus, doesn't prevent the it from being garbage collected.
// Note: WPF & UWP *do* require disposal in this scenario, thanks to the way
// dependency properties work.
this.WhenAnyValue(x => x.ViewModel)
.Where(x => x != null)
.Do(PopulateFromViewModel)
.Subscribe();
}
private void PopulateFromViewModel(MasterCellViewModel viewModel)
{
// Because menu items usually don't change for the lifetime of an app (for most use cases),
// set the values directly instead of binding, for better performance.
// If your ViewModel properties don't change over time, definitely use this pattern.
TitleLabel.Text = viewModel.Title;
IconImage.Source = viewModel.IconSource;
}
}
}

Просмотреть файл

@ -0,0 +1,13 @@
using System;
namespace MasterDetail
{
public class MasterCellViewModel
{
public string Title { get; set; }
public string IconSource { get; set; }
public Type TargetType { get; set; }
}
}

Просмотреть файл

@ -4,29 +4,9 @@
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<Compile Remove="DynamicMasterDetail\**" />
<EmbeddedResource Remove="DynamicMasterDetail\**" />
<None Remove="DynamicMasterDetail\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI" Version="*" />
<PackageReference Include="ReactiveUI.XamForms" Version="*" />
<PackageReference Include="ReactiveUI" Version="8.7.2" />
<PackageReference Include="ReactiveUI.XamForms" Version="8.7.2" />
<PackageReference Include="Xamarin.Forms" Version="3.1.0.697729" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="CustomCell.xaml">
<Generator>MSBuild:Compile</Generator>
</EmbeddedResource>
<EmbeddedResource Update="MyDetailPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>

Просмотреть файл

@ -1,36 +0,0 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using ReactiveUI;
using ReactiveUI.XamForms;
using Xamarin.Forms.Xaml;
namespace MasterDetail
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MyDetailPage : ReactiveContentPage<MyDetailViewModel>
{
public MyDetailPage(MyDetailViewModel viewModel)
{
InitializeComponent();
ViewModel = viewModel;
this.WhenActivated(
disposables =>
{
this
.WhenAnyValue(x => x.ViewModel.Model)
.Where(x => x != null)
.Subscribe(model => PopulateFromModel(model))
.DisposeWith(disposables);
});
}
private void PopulateFromModel(MyModel model)
{
Title = model.Title;
TitleLabel.Text = model.Title;
}
}
}

Просмотреть файл

@ -1,17 +0,0 @@
using ReactiveUI;
namespace MasterDetail
{
public class MyDetailViewModel : ReactiveObject
{
private MyModel _model;
public MyModel Model
{
get => _model;
set => this.RaiseAndSetIfChanged(ref _model, value);
}
public string Title => Model.Title;
}
}

Просмотреть файл

@ -1,9 +0,0 @@
namespace MasterDetail
{
public class MyModel
{
public string Title { get; set; }
public string IconSource { get; set; }
}
}

Просмотреть файл

@ -148,7 +148,7 @@
<Reference Include="Xamarin.iOS" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ReactiveUI" Version="*" />
<PackageReference Include="ReactiveUI" Version="8.7.2" />
<PackageReference Include="Xamarin.Forms" Version="3.1.0.697729" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />