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

Commit 2353ab1

Browse files
authored
Merge pull request #1367 from github/refactor/mvvm
MVVM-ize things (attempt 2)
2 parents 3319127 + 86ec14b commit 2353ab1

File tree

196 files changed

+4135
-5271
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

196 files changed

+4135
-5271
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Dialog Views with Connections
2+
3+
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.
4+
5+
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:
6+
7+
```csharp
8+
public Task InitializeAsync(IConnection connection)
9+
{
10+
// .. at this point, you're guaranteed to have a connection.
11+
}
12+
```
13+
14+
To show the dialog, call `IShowDialogService.ShowWithFirstConnection` instead of `Show`:
15+
16+
```csharp
17+
public async Task ShowExampleDialog()
18+
{
19+
var viewModel = serviceProvider.ExportProvider.GetExportedValue<IExampleDialogViewModel>();
20+
await showDialog.ShowWithFirstConnection(viewModel);
21+
}
22+
```
23+
24+
`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.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# How ViewModels are Turned into Views
2+
3+
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.
4+
5+
## DataTemplates
6+
7+
[`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:
8+
9+
```csharp
10+
public class ViewModel
11+
{
12+
public string Greeting => "Hello World!";
13+
}
14+
```
15+
16+
And a window:
17+
18+
```csharp
19+
public class MainWindow : Window
20+
{
21+
public MainWindow()
22+
{
23+
DataContext = new ViewModel();
24+
InitializeComponent();
25+
}
26+
}
27+
```
28+
29+
```xml
30+
<Window x:Class="MyApp.MainWindow"
31+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
32+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
33+
xmlns:local="clr-namespace:MyApp"
34+
Content="{Binding}">
35+
<Window>
36+
37+
```
38+
39+
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`.
40+
41+
One can choose to display the `ViewModel` instance in any way we want by using a `DataTemplate`:
42+
43+
```xml
44+
<Window x:Class="MyApp.MainWindow"
45+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
46+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
47+
xmlns:local="clr-namespace:MyApp"
48+
Content="{Binding}">
49+
<Window.Resources>
50+
<DataTemplate DataType="{x:Type local:ViewModel}">
51+
52+
<!-- Display ViewModel.Greeting in a red border with rounded corners -->
53+
<Border Background="Red" CornerRadius="8">
54+
<TextBlock Binding="{Binding Greeting}"/>
55+
</Border>
56+
57+
</DataTemplate>
58+
</Window.Resources>
59+
</Window>
60+
```
61+
62+
This is the basis for converting view models to views.
63+
64+
## ViewLocator
65+
66+
There are currently two top-level controls for our UI:
67+
68+
- [GitHubDialogWindow](../src/GitHub.VisualStudio/Views/Dialog/GitHubDialogWindow.xaml) for the dialog which shows the login, clone, etc views
69+
- [GitHubPaneView](../src/GitHub.VisualStudio/Views/GitHubPane/GitHubPaneView.xaml) for the GitHub pane
70+
71+
In the resources for each of these top-level controls we define a `DataTemplate` like so:
72+
73+
```xml
74+
<views:ViewLocator x:Key="viewLocator"/>
75+
<DataTemplate DataType="{x:Type vm:ViewModelBase}">
76+
<ContentControl Content="{Binding Converter={StaticResource viewLocator}}"/>
77+
</DataTemplate>
78+
```
79+
80+
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`.
81+
82+
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.
83+
84+
And thus a view model becomes a view.
85+
86+
[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.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Implementing a Dialog View
2+
3+
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:
4+
5+
## Create a View Model and Interface
6+
7+
- Create an interface for the view model that implements `IDialogContentViewModel` in `GitHub.Exports.Reactive\ViewModels\Dialog`
8+
- Create a view model that inherits from `NewViewModelBase` and implements the interface in `GitHub.App\ViewModels\Dialog`
9+
- Export the view model with the interface as the contract and add a `[PartCreationPolicy(CreationPolicy.NonShared)]` attribute
10+
11+
A minimal example that just exposes a command that will dismiss the dialog:
12+
13+
```csharp
14+
using System;
15+
using ReactiveUI;
16+
17+
namespace GitHub.ViewModels.Dialog
18+
{
19+
public interface IExampleDialogViewModel : IDialogContentViewModel
20+
{
21+
ReactiveCommand<object> Dismiss { get; }
22+
}
23+
}
24+
```
25+
26+
```csharp
27+
using System;
28+
using System.ComponentModel.Composition;
29+
using ReactiveUI;
30+
31+
namespace GitHub.ViewModels.Dialog
32+
{
33+
[Export(typeof(IExampleDialogViewModel))]
34+
[PartCreationPolicy(CreationPolicy.NonShared)]
35+
public class ExampleDialogViewModel : ViewModelBase, IExampleDialogViewModel
36+
{
37+
[ImportingConstructor]
38+
public ExampleDialogViewModel()
39+
{
40+
Dismiss = ReactiveCommand.Create();
41+
}
42+
43+
public string Title => "Example Dialog";
44+
45+
public ReactiveCommand<object> Dismiss { get; }
46+
47+
public IObservable<object> Done => Dismiss;
48+
}
49+
}
50+
```
51+
52+
## Create a View
53+
54+
- Create a WPF `UserControl` under `GitHub.VisualStudio\Views\Dialog`
55+
- Add an `ExportViewFor` attribute with the type of the view model interface
56+
- Add a `PartCreationPolicy(CreationPolicy.NonShared)]` attribute
57+
58+
Continuing the example above:
59+
60+
```xml
61+
<UserControl x:Class="GitHub.VisualStudio.Views.Dialog.ExampleDialogView"
62+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
63+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
64+
<Button Command="{Binding Dismiss}" HorizontalAlignment="Center" VerticalAlignment="Center">
65+
Dismiss
66+
</Button>
67+
</UserControl>
68+
```
69+
70+
```csharp
71+
using System.ComponentModel.Composition;
72+
using System.Windows.Controls;
73+
using GitHub.Exports;
74+
using GitHub.ViewModels.Dialog;
75+
76+
namespace GitHub.VisualStudio.Views.Dialog
77+
{
78+
[ExportViewFor(typeof(IExampleDialogViewModel))]
79+
[PartCreationPolicy(CreationPolicy.NonShared)]
80+
public partial class ExampleDialogView : UserControl
81+
{
82+
public ExampleDialogView()
83+
{
84+
InitializeComponent();
85+
}
86+
}
87+
}
88+
```
89+
90+
## Show the Dialog!
91+
92+
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.
93+
94+
```csharp
95+
var viewModel = new ExampleDialogViewModel();
96+
showDialog.Show(viewModel)
97+
```
98+
99+
## Optional: Add a method to `DialogService`
100+
101+
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`:
102+
103+
```csharp
104+
public async Task ShowExampleDialog()
105+
{
106+
var viewModel = factory.CreateViewModel<IExampleDialogViewModel>();
107+
await showDialog.Show(viewModel);
108+
}
109+
```
110+
111+
Obviously, add this method to `IDialogService` too.
112+
113+
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`.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Implementing a GitHub Pane Page
2+
3+
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:
4+
5+
## Create a View Model and Interface
6+
7+
- Create an interface for the view model that implements `IPanePageViewModel` in `GitHub.Exports.Reactive\ViewModels\GitHubPane`
8+
- Create a view model that inherits from `PanePageViewModelBase` and implements the interface in `GitHub.App\ViewModels\GitHubPane`
9+
- Export the view model with the interface as the contract and add a `[PartCreationPolicy(CreationPolicy.NonShared)]` attribute
10+
11+
A minimal example that just exposes a command that will navigate to the pull request list:
12+
13+
```csharp
14+
using System;
15+
using ReactiveUI;
16+
17+
namespace GitHub.ViewModels.GitHubPane
18+
{
19+
public interface IExamplePaneViewModel : IPanePageViewModel
20+
{
21+
ReactiveCommand<object> GoToPullRequests { get; }
22+
}
23+
}
24+
```
25+
26+
```csharp
27+
using System;
28+
using System.ComponentModel.Composition;
29+
using ReactiveUI;
30+
31+
namespace GitHub.ViewModels.GitHubPane
32+
{
33+
[Export(typeof(IExamplePaneViewModel))]
34+
[PartCreationPolicy(CreationPolicy.NonShared)]
35+
public class ExamplePaneViewModel : PanePageViewModelBase, IExamplePaneViewModel
36+
{
37+
[ImportingConstructor]
38+
public ExamplePaneViewModel()
39+
{
40+
GoToPullRequests = ReactiveCommand.Create();
41+
GoToPullRequests.Subscribe(_ => NavigateTo("/pulls"));
42+
}
43+
44+
public ReactiveCommand<object> GoToPullRequests { get; }
45+
}
46+
}
47+
```
48+
49+
## Create a View
50+
51+
- Create a WPF `UserControl` under `GitHub.VisualStudio\ViewsGitHubPane`
52+
- Add an `ExportViewFor` attribute with the type of the view model interface
53+
- Add a `PartCreationPolicy(CreationPolicy.NonShared)]` attribute
54+
55+
Continuing the example above:
56+
57+
```xml
58+
<UserControl x:Class="GitHub.VisualStudio.Views.GitHubPane.ExamplePaneView"
59+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
60+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
61+
<Button Command="{Binding GoToPullRequests}"
62+
HorizontalAlignment="Center"
63+
VerticalAlignment="Center">
64+
Go to Pull Requests
65+
</Button>
66+
</UserControl>
67+
68+
```
69+
70+
```csharp
71+
using System.ComponentModel.Composition;
72+
using System.Windows.Controls;
73+
using GitHub.Exports;
74+
using GitHub.ViewModels.Dialog;
75+
76+
namespace GitHub.VisualStudio.Views.Dialog
77+
{
78+
[ExportViewFor(typeof(IExampleDialogViewModel))]
79+
[PartCreationPolicy(CreationPolicy.NonShared)]
80+
public partial class ExampleDialogView : UserControl
81+
{
82+
public ExampleDialogView()
83+
{
84+
InitializeComponent();
85+
}
86+
}
87+
}
88+
```
89+
90+
## Add a Route to GitHubPaneViewModel
91+
92+
Now you need to add a route to the `GitHubPaneViewModel`. To add a route, you must do two things:
93+
94+
- Add a method to `GitHubPaneViewModel`
95+
- Add a URL handler to `GitHubPaneViewModel.NavigateTo`
96+
97+
So lets add the `ShowExample` method to `GitHubPaneViewModel`:
98+
99+
```csharp
100+
public Task ShowExample()
101+
{
102+
return NavigateTo<IExamplePaneViewModel>(x => Task.CompletedTask);
103+
}
104+
```
105+
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.
106+
107+
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`:
108+
109+
```csharp
110+
else if (uri.AbsolutePath == "/example")
111+
{
112+
await ShowExample();
113+
}
114+
```
115+
116+
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:
117+
118+
```csharp
119+
public Task ShowDefaultPage() => ShowExample();
120+
```
121+
122+
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.

0 commit comments

Comments
 (0)