-
Notifications
You must be signed in to change notification settings - Fork 1.2k
MVVM-ize things #1346
MVVM-ize things #1346
Changes from 18 commits
5106ff5
c71b2ab
1a886bf
8c2f286
c50b29a
91703a4
d928d72
a3fe54b
c578ecd
42c1d34
16df7cc
7273371
e7c4ef0
22c8345
9ee5f0e
f8e48fb
304485e
0dac942
6f4fe80
6c4ff1b
c3d5335
81e29d3
1c24639
67a8a8c
c1c58c5
5372cad
b93c6f9
29b0536
68fd076
2a85875
2eb687c
f771bb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Dialog Views with Connections | ||
|
|
||
| Some dialog views need a connection to operate - if there is no connection, a login dialog should be shown: for example, clicking Create Gist without a connection will first prompt the user to log in. | ||
|
|
||
| Achieving this is simple, first make your view model interface implement `IConnectionInitializedViewModel` and do any initialization that requires a connection in the `InitializeAsync` method in your view model: | ||
|
|
||
| ```csharp | ||
| public Task InitializeAsync(IConnection connection) | ||
| { | ||
| // .. at this point, you're guaranteed to have a connection. | ||
| } | ||
| ``` | ||
|
|
||
| To show the dialog, call `IShowDialogService.ShowWithFirstConnection` instead of `Show`: | ||
|
|
||
| ```csharp | ||
| public async Task ShowExampleDialog() | ||
| { | ||
| var viewModel = serviceProvider.ExportProvider.GetExportedValue<IExampleDialogViewModel>(); | ||
| await showDialog.ShowWithFirstConnection(viewModel); | ||
| } | ||
| ``` | ||
|
|
||
| `ShowFirstConnection` first checks if there are any logged in connections. If there are, the first logged in connection will be passed to `InitializeAsync` and the view shown immediately. If there are no logged in connections, the login view will first be shown. Once the user has successfully logged in, the new connection will be passed to `InitalizeAsync` and the view shown. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # How ViewModels are Turned into Views | ||
|
|
||
| We make use of the [MVVM pattern](https://msdn.microsoft.com/en-us/library/ff798384.aspx), in which application level code is not aware of the view level. MVVM takes advantage of the fact that `DataTemplate`s can be used to create views from view models. | ||
|
|
||
| ## DataTemplates | ||
|
|
||
| [`DataTemplate`](https://docs.microsoft.com/en-us/dotnet/framework/wpf/data/data-templating-overview)s are a WPF feature that allow you to define the presentation of your data. Consider a simple view model: | ||
|
|
||
| ```csharp | ||
| public class ViewModel | ||
| { | ||
| public string Greeting => "Hello World!"; | ||
| } | ||
| ``` | ||
|
|
||
| And a window: | ||
|
|
||
| ```csharp | ||
| public class MainWindow : Window | ||
| { | ||
| public MainWindow() | ||
| { | ||
| DataContext = new ViewModel(); | ||
| InitializeComponent(); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ```xml | ||
| <Window x:Class="MyApp.MainWindow" | ||
| xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||
| xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||
| xmlns:local="clr-namespace:MyApp" | ||
| Content="{Binding}"> | ||
| </pfui:DialogWindow> | ||
|
|
||
| ``` | ||
|
|
||
| Here we're binding the `Content` of the `Window` to the `Window.DataContext`, which we're setting in the constructor to be an instance of `ViewModel`. | ||
|
|
||
| One can choose to display the `ViewModel` instance in any way we want by using a `DataTemplate`: | ||
|
|
||
| ```xml | ||
| <Window x:Class="MyApp.MainWindow" | ||
| xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||
| xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||
| xmlns:local="clr-namespace:MyApp" | ||
| Content="{Binding}"> | ||
| <Window.Resources> | ||
| <DataTemplate DataType="{x:Type local:ViewModel}"> | ||
|
|
||
| <!-- Display ViewModel.Greeting in a red border with rounded corners --> | ||
| <Border Background="Red" CornerRadius="8"> | ||
| <TextBlock Binding="{Binding Greeting}" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing a |
||
| </Border> | ||
|
|
||
| </DataTemplate> | ||
| </Window.Resources> | ||
| </pfui:DialogWindow> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be |
||
| ``` | ||
|
|
||
| This is the basis for converting view models to views. | ||
|
|
||
| ## ViewLocator | ||
|
|
||
| There are currently two top-level controls for our UI: | ||
|
|
||
| - [GitHubDialogWindow](../src/GitHub.VisualStudio/Views/Dialog/GitHubDialogWindow.xaml) for the dialog which shows the login, clone, etc views | ||
| - [GitHubPaneView](../src/GitHub.VisualStudio/Views/GitHubPane/GitHubPaneView.xaml) for the GitHub pane | ||
|
|
||
| In the resources for each of these top-level controls we define a `DataTemplate` like so: | ||
|
|
||
| ```xml | ||
| <views:ViewLocator x:Key="viewLocator"/> | ||
| <DataTemplate DataType="{x:Type vm:ViewModelBase}"> | ||
| <ContentControl Content="{Binding Converter={StaticResource viewLocator}}"/> | ||
| </DataTemplate> | ||
| ``` | ||
|
|
||
| The `DataTemplate.DataType` here applies the template to all classes inherited from [`GitHub.ViewModels.ViewModelBase`](../src/GitHub.Exports.Reactive/ViewModels/ViewModelBase.cs) [1]. The template defines a single `ContentControl` whose contents are created by a `ViewLocator`. | ||
|
|
||
| The [`ViewLocator`](../src/GitHub.VisualStudio/Views/ViewLocator.cs) class is an `IValueConverter` which then creates an instance of the appropriate view for the view model using MEF. | ||
|
|
||
| And thus a view model becomes a view. | ||
|
|
||
| [1]: it would be nice to make it apply to all classes that inherit `IViewModel` but unfortunately WPF's `DataTemplate`s don't work with interfaces. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sure I'm missing something, but is this not a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, interesting, I didn't know this was possible! Worth a try! |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| # Implementing a Dialog View | ||
|
|
||
| GitHub for Visual Studio has a common dialog which is used to show the login, clone, create repository etc. operations. To add a new view to the dialog and show the dialog with this view, you need to do the following: | ||
|
|
||
| ## Create a View Model and Interface | ||
|
|
||
| > TODO: `NewViewModelBase` will become simply `ViewModelBase` once the MVVM refactor is completed. | ||
|
|
||
| - Create an interface for the view model that implements `IDialogContentViewModel` in `GitHub.Exports.Reactive\ViewModels\Dialog` | ||
| - Create a view model that inherits from `NewViewModelBase` and implements the interface in `GitHub.App\ViewModels\Dialog` | ||
| - Export the view model with the interface as the contract and add a `[PartCreationPolicy(CreationPolicy.NonShared)]` attribute | ||
|
|
||
| A minimal example that just exposes a command that will dismiss the dialog: | ||
|
|
||
| ```csharp | ||
| using System; | ||
| using ReactiveUI; | ||
|
|
||
| namespace GitHub.ViewModels.Dialog | ||
| { | ||
| public interface IExampleDialogViewModel : IDialogContentViewModel | ||
| { | ||
| ReactiveCommand<object> Dismiss { get; } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ```csharp | ||
| using System; | ||
| using System.ComponentModel.Composition; | ||
| using ReactiveUI; | ||
|
|
||
| namespace GitHub.ViewModels.Dialog | ||
| { | ||
| [Export(typeof(IExampleDialogViewModel))] | ||
| [PartCreationPolicy(CreationPolicy.NonShared)] | ||
| public class ExampleDialogViewModel : NewViewModelBase, IExampleDialogViewModel | ||
| { | ||
| [ImportingConstructor] | ||
| public ExampleDialogViewModel() | ||
| { | ||
| Dismiss = ReactiveCommand.Create(); | ||
| } | ||
|
|
||
| public string Title => "Example Dialog"; | ||
|
|
||
| public ReactiveCommand<object> Dismiss { get; } | ||
|
|
||
| public IObservable<object> Done => Dismiss; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Create a View | ||
|
|
||
| > TODO: Decide if `GitHub.VisualStudio\Views` is the best place for views | ||
|
|
||
| - Create a WPF `UserControl` under `GitHub.VisualStudio\Views\Dialog` | ||
| - Add an `ExportViewFor` attribute with the type of the view model interface | ||
| - Add a `PartCreationPolicy(CreationPolicy.NonShared)]` attribute | ||
|
|
||
| Continuing the example above: | ||
|
|
||
| ```xml | ||
| <UserControl x:Class="GitHub.VisualStudio.Views.Dialog.ExampleDialogView" | ||
| xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||
| xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> | ||
| <Button Command="{Binding Dismiss}" HorizontalAlignment="Center" VerticalAlignment="Center"> | ||
| Dismiss | ||
| </Button> | ||
| </UserControl> | ||
| ``` | ||
|
|
||
| ```csharp | ||
| using System.ComponentModel.Composition; | ||
| using System.Windows.Controls; | ||
| using GitHub.Exports; | ||
| using GitHub.ViewModels.Dialog; | ||
|
|
||
| namespace GitHub.VisualStudio.Views.Dialog | ||
| { | ||
| [ExportViewFor(typeof(IExampleDialogViewModel))] | ||
| [PartCreationPolicy(CreationPolicy.NonShared)] | ||
| public partial class ExampleDialogView : UserControl | ||
| { | ||
| public ExampleDialogView() | ||
| { | ||
| InitializeComponent(); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Show the Dialog! | ||
|
|
||
| To show the dialog you will need an instance of the `IShowDialog` service. Once you have that, simply call the `Show` method with an instance of your view model. | ||
|
|
||
| ```csharp | ||
| var viewModel = new ExampleDialogViewModel(); | ||
| showDialog.Show(viewModel) | ||
| ``` | ||
|
|
||
| ## Optional: Add a method to `DialogService` | ||
|
|
||
| Creating a view model like this may be the right thing to do, but it's not very reusable or testable. If you want your dialog to be easy reusable, add a method to `DialogService`: | ||
|
|
||
| ```csharp | ||
| public async Task ShowExampleDialog() | ||
| { | ||
| var viewModel = factory.CreateViewModel<IExampleDialogViewModel>(); | ||
| await showDialog.Show(viewModel); | ||
| } | ||
| ``` | ||
|
|
||
| Obviously, add this method to `IDialogService` too. | ||
|
|
||
| Note that these methods are `async` - this means that if you need to do asynchronous initialization of your view model, you can do it here before calling `showDialog`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| # Multi-paged Dialogs | ||
|
|
||
| Some dialogs will be multi-paged - for example the login dialog has a credentials page and a 2Fa page that is shown if two-factor authorization is required. | ||
|
|
||
| ## The View Model | ||
|
|
||
| To help implement view models for a multi-page dialog there is a useful base class called `PagedDialogViewModelBase`. The typical way of implementing this is as follows: | ||
|
|
||
| - Define each page of the dialog as you would [implement a single dialog view model](implementing-a-dialog-view.md) | ||
| - Implement a "container" view model for the dialog that inherits from `PagedDialogViewModel` | ||
| - Import each page into the container view model | ||
| - Add logic to switch between pages by setting the `PagedDialogViewModelBase.Content` property | ||
| - Add a `Done` observable | ||
|
|
||
| Here's a simple example of a container dialog that has two pages. The pages are switched using `ReactiveCommand`s: | ||
|
|
||
| ```csharp | ||
| using System; | ||
| using System.ComponentModel.Composition; | ||
|
|
||
| namespace GitHub.ViewModels.Dialog | ||
| { | ||
| [Export(typeof(IExamplePagedDialogViewModel))] | ||
| [PartCreationPolicy(CreationPolicy.NonShared)] | ||
| public class ExamplePagedDialogViewModel : PagedDialogViewModelBase, | ||
| IExamplePagedDialogViewModel | ||
| { | ||
| [ImportingConstructor] | ||
| public ExamplePagedDialogViewModel( | ||
| IPage1ViewModel page1, | ||
| IPage2ViewModel page2) | ||
| { | ||
| Content = page1; | ||
| page1.Next.Subscribe(_ => Content = page2); | ||
| page2.Previous.Subscribe(_ => Content = page1); | ||
| Done = Observable.Merge(page2.Done, page2.Done); | ||
| } | ||
|
|
||
| public override IObservable<object> Done { get; } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## The View | ||
|
|
||
| The view in this case is very simple: it just needs to display the `Content` property of the container view model: | ||
|
|
||
| ```xml | ||
| <UserControl x:Class="GitHub.VisualStudio.Views.Dialog.ExamplePagedDialogView" | ||
| xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||
| xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||
| Content="{Binding Content}"> | ||
| </UserControl> | ||
| ``` | ||
|
|
||
| ```csharp | ||
| using System; | ||
| using System.ComponentModel.Composition; | ||
| using System.Windows.Controls; | ||
| using GitHub.Exports; | ||
| using GitHub.ViewModels.Dialog; | ||
|
|
||
| namespace GitHub.VisualStudio.Views.Dialog | ||
| { | ||
| [ExportViewFor(typeof(IExamplePagedDialogViewModel))] | ||
| [PartCreationPolicy(CreationPolicy.NonShared)] | ||
| public partial class ExamplePagedDialogView : UserControl | ||
| { | ||
| public NewLoginView() | ||
| { | ||
| InitializeComponent(); | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| > Note: this is such a common pattern, you don't actually need to define your own view! Simply add the `[ExportViewFor(...)]` attribute to the existing `ContentView` class. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # Developer Documentation | ||
|
|
||
| Documentation for hacking on GitHub for Visual Studio: | ||
|
|
||
| - User Interface | ||
| - [How ViewModels are Turned into Views](how-viewmodels-are-turned-into-views.md) | ||
| - [Implementing a Dialog View](implementing-a-dialog-view.md) | ||
| - [Dialog Views with Connections](dialog-views-with-connections.md) | ||
| - [Multi-Paged Dialogs](multi-paged-dialogs.md) | ||
|
|
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be
</Window>?