This repository was archived by the owner on Jun 21, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
MVVM-ize things #1346
Merged
grokys
merged 32 commits into
refactor/1352-remove-designtimestylehelper
from
refactor/mvvm
Dec 5, 2017
Merged
MVVM-ize things #1346
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
5106ff5
Rebase of MVVM refactor.
grokys c71b2ab
Added some doc comments.
grokys 1a886bf
Use IViewViewModelFactory...
grokys 8c2f286
Add DialogService to MenuBase.
grokys c50b29a
Fix compile error.
grokys 91703a4
Removed unused types.
grokys d928d72
Added move developer docs.
grokys a3fe54b
Include VSGitExt in GitHub.TeamFoundation.15.
grokys c578ecd
Merge branch 'fixes/1355-vsgitext-vs2017' into refactor/mvvm
grokys 42c1d34
Added progress bar to Clone dialog page.
grokys 16df7cc
Added general purpose ContentView.
grokys 7273371
Supress CA error.`
grokys e7c4ef0
Doc comments.
grokys 22c8345
Don't add current page to current page.
grokys 9ee5f0e
Display error messages in GitHubPane.
grokys f8e48fb
Fix unit tests.
grokys 304485e
Fixed spinner showing when logged out.
grokys 0dac942
Fix DataContext/ViewModel property sync.
grokys 6f4fe80
Fixed XAML errors in docs.
grokys 6c4ff1b
Refresh PR detail view when active repo changes
jcansdale c3d5335
Make PullRequestDetailViewModel sealed
jcansdale 81e29d3
Don't worry about auto-disposing
jcansdale 1c24639
Show title in dialog window.
grokys 67a8a8c
Doc fixes.
grokys c1c58c5
Added ShowDefaultPage to GitHubPaneViewModel.
grokys 5372cad
Added docs for implementing a pane page.
grokys b93c6f9
Expose `IGitHubPaneViewModel.NavigateTo(Uri)`.
grokys 29b0536
Merge pull request #1359 from github/fixes/1345-refresh-PR-detail-view
grokys 68fd076
Merge branch 'master' into refactor/mvvm
grokys 2a85875
Fix broken tests.
grokys 2eb687c
Switch to UI thread in ActiveRepositoriesChanged.
grokys f771bb8
Suppress CA error.
grokys File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"> | ||
<Window> | ||
|
||
``` | ||
|
||
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}"/> | ||
</Border> | ||
|
||
</DataTemplate> | ||
</Window.Resources> | ||
</Window> | ||
``` | ||
|
||
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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
# 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 | ||
|
||
- 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 : ViewModelBase, 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 | ||
|
||
- 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 `IShowDialogService` 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`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
# Implementing a GitHub Pane Page | ||
|
||
The GitHub pane displays GitHub-specific functionality in a dockable pane. To add a new page to the GitHub pane, you need to do the following: | ||
|
||
## Create a View Model and Interface | ||
|
||
- Create an interface for the view model that implements `IPanePageViewModel` in `GitHub.Exports.Reactive\ViewModels\GitHubPane` | ||
- Create a view model that inherits from `PanePageViewModelBase` and implements the interface in `GitHub.App\ViewModels\GitHubPane` | ||
- 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 navigate to the pull request list: | ||
|
||
```csharp | ||
using System; | ||
using ReactiveUI; | ||
|
||
namespace GitHub.ViewModels.GitHubPane | ||
{ | ||
public interface IExamplePaneViewModel : IPanePageViewModel | ||
{ | ||
ReactiveCommand<object> GoToPullRequests { get; } | ||
} | ||
} | ||
``` | ||
|
||
```csharp | ||
using System; | ||
using System.ComponentModel.Composition; | ||
using ReactiveUI; | ||
|
||
namespace GitHub.ViewModels.GitHubPane | ||
{ | ||
[Export(typeof(IExamplePaneViewModel))] | ||
[PartCreationPolicy(CreationPolicy.NonShared)] | ||
public class ExamplePaneViewModel : PanePageViewModelBase, IExamplePaneViewModel | ||
{ | ||
[ImportingConstructor] | ||
public ExamplePaneViewModel() | ||
{ | ||
GoToPullRequests = ReactiveCommand.Create(); | ||
GoToPullRequests.Subscribe(_ => NavigateTo("/pulls")); | ||
} | ||
|
||
public ReactiveCommand<object> GoToPullRequests { get; } | ||
} | ||
} | ||
``` | ||
|
||
## Create a View | ||
|
||
- Create a WPF `UserControl` under `GitHub.VisualStudio\ViewsGitHubPane` | ||
- 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.GitHubPane.ExamplePaneView" | ||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> | ||
<Button Command="{Binding GoToPullRequests}" | ||
HorizontalAlignment="Center" | ||
VerticalAlignment="Center"> | ||
Go to Pull Requests | ||
</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(); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
## Add a Route to GitHubPaneViewModel | ||
|
||
Now you need to add a route to the `GitHubPaneViewModel`. To add a route, you must do two things: | ||
|
||
- Add a method to `GitHubPaneViewModel` | ||
- Add a URL handler to `GitHubPaneViewModel.NavigateTo` | ||
|
||
So lets add the `ShowExample` method to `GitHubPaneViewModel`: | ||
|
||
```csharp | ||
public Task ShowExample() | ||
{ | ||
return NavigateTo<IExamplePaneViewModel>(x => Task.CompletedTask); | ||
} | ||
``` | ||
Here we call `NavigateTo` with the type of the interface of our view model. We're passing a lambda that simply returns `Task.CompletedTask` as the parameter: usually here you'd call an async initialization method on the view model, but since we don't have one in our simple example we just return a completed task. | ||
|
||
Next we add a URL handler: our URL is going to be `github://pane/example` so we need to add a route that checks that the URL's `AbsolutePath` is `/example` and if so call the method we added above. This code is added to `GitHubPaneViewModel.NavigateTo`: | ||
|
||
```csharp | ||
else if (uri.AbsolutePath == "/example") | ||
{ | ||
await ShowExample(); | ||
} | ||
``` | ||
|
||
For the sake of the example, we're going to show our new page as soon as the GitHub Pane is shown and the user is logged-in with an open repository. To do this, simply change `GitHubPaneViewModel.ShowDefaultPage` to the following: | ||
|
||
```csharp | ||
public Task ShowDefaultPage() => ShowExample(); | ||
``` | ||
|
||
When you run the extension and show the GitHub pane, our new example page should be shown. Clicking on the button in the page will navigate to the pull request list. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I'm sure I'm missing something, but is this not a
DataTemplate
that uses an interface?https://stackoverflow.com/a/25642988/121348
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.
Hmm, interesting, I didn't know this was possible! Worth a try!