Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

MVVM-ize things #1346

Merged
merged 32 commits into from
Dec 5, 2017
Merged
Show file tree
Hide file tree
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 Nov 28, 2017
c71b2ab
Added some doc comments.
grokys Nov 28, 2017
1a886bf
Use IViewViewModelFactory...
grokys Nov 28, 2017
8c2f286
Add DialogService to MenuBase.
grokys Nov 28, 2017
c50b29a
Fix compile error.
grokys Nov 28, 2017
91703a4
Removed unused types.
grokys Nov 28, 2017
d928d72
Added move developer docs.
grokys Nov 29, 2017
a3fe54b
Include VSGitExt in GitHub.TeamFoundation.15.
grokys Nov 29, 2017
c578ecd
Merge branch 'fixes/1355-vsgitext-vs2017' into refactor/mvvm
grokys Nov 29, 2017
42c1d34
Added progress bar to Clone dialog page.
grokys Nov 29, 2017
16df7cc
Added general purpose ContentView.
grokys Nov 29, 2017
7273371
Supress CA error.`
grokys Nov 29, 2017
e7c4ef0
Doc comments.
grokys Nov 29, 2017
22c8345
Don't add current page to current page.
grokys Nov 29, 2017
9ee5f0e
Display error messages in GitHubPane.
grokys Nov 29, 2017
f8e48fb
Fix unit tests.
grokys Nov 29, 2017
304485e
Fixed spinner showing when logged out.
grokys Nov 30, 2017
0dac942
Fix DataContext/ViewModel property sync.
grokys Nov 30, 2017
6f4fe80
Fixed XAML errors in docs.
grokys Dec 1, 2017
6c4ff1b
Refresh PR detail view when active repo changes
jcansdale Dec 1, 2017
c3d5335
Make PullRequestDetailViewModel sealed
jcansdale Dec 1, 2017
81e29d3
Don't worry about auto-disposing
jcansdale Dec 4, 2017
1c24639
Show title in dialog window.
grokys Dec 4, 2017
67a8a8c
Doc fixes.
grokys Dec 4, 2017
c1c58c5
Added ShowDefaultPage to GitHubPaneViewModel.
grokys Dec 4, 2017
5372cad
Added docs for implementing a pane page.
grokys Dec 4, 2017
b93c6f9
Expose `IGitHubPaneViewModel.NavigateTo(Uri)`.
grokys Dec 4, 2017
29b0536
Merge pull request #1359 from github/fixes/1345-refresh-PR-detail-view
grokys Dec 4, 2017
68fd076
Merge branch 'master' into refactor/mvvm
grokys Dec 4, 2017
2a85875
Fix broken tests.
grokys Dec 4, 2017
2eb687c
Switch to UI thread in ActiveRepositoriesChanged.
grokys Dec 4, 2017
f771bb8
Suppress CA error.
grokys Dec 5, 2017
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
24 changes: 24 additions & 0 deletions docs/developer/dialog-views-with-connections.md
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.
86 changes: 86 additions & 0 deletions docs/developer/how-viewmodels-are-turned-into-views.md
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.
Copy link
Collaborator

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

Copy link
Contributor Author

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!

113 changes: 113 additions & 0 deletions docs/developer/implementing-a-dialog-view.md
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`.
122 changes: 122 additions & 0 deletions docs/developer/implementing-github-pane-page.md
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.
Loading