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

Conversation

grokys
Copy link
Contributor

@grokys grokys commented Nov 28, 2017

This pull request migrates our code to use the MVVM pattern throughout. MVVM is generally considered to be the "right" way to do WPF and greatly improves the unit-testability of code by separating application layer and view layer code.

This is a big PR, sorry! To ease reviewing, the following attempts to explain the major changes and concepts.

UIController Removed

UIController was previously responsible for creating views and view models based on the UIViewType enumeration.

  • There is no concept equivalent to UIController after the refactor; it's view models all the way down!
  • Instead of UIViewType, view models and views are keyed on the type of the view model interface

NavigationController Removed

NavigationController was built on UIController to display the browser-style user interface in the GitHub pane. The equivalent now is NavigationViewModel, however this simply maintains a list of pages and a position in that list, it knows nothing about the pages it will display. That responsibility is handled by:

GitHubPaneViewModel

GitHubPaneViewModel exposes a Content property which:

  • When the user is logged-in and a GitHub repository is open, will be a NavigationViewModel
  • When the user is not logged in or there's not a GitHub repository open then the Content will be a view model representing this state such as an ILoggedOutViewModel

GitHubPaneViewModel supports navigation via URLs, such as github://pane/owner/repo/pull/5 (via NavigateTo(Uri)) or by calling a method such as ShowPullRequest("owner", "repo", 5).

Asynchronous Initialization

Previously view model initialization methods were all of the signature void Initialize(ViewWithData) (this was defined in the IViewModel base interface). This had three downsides:

  • Type-safely was lost as everything had to be cast to an object and placed in ViewWithData.Data
  • Asynchronous initialization was difficult
  • It mixed view and view model concerns

The base IViewModel interface no longer defines a one-fits-all initialization method and view models are now expected to define a (probably async) initialization method in their interface which must be called following creation.

Many of the changes in this PR consist of moving things from the ViewModel constructor to an async initialization method.

ViewLocator

At the root of each GitHub top-level control (i.e. dialog and the GitHub pane) there is a DataTemplate typed on ViewModelBase. This DataTemplate has a ViewLocator as its converter and is used to convert view models to their equivalent views. Using this technique the application layer code need have no concept of the view layer: the application state is built purely with view models which are converted to equivalent views by the view layer.

See the documentation here.

Views and View Models Moved

Previously all views and view models were all in the same folder/namespace, which was starting to get unwieldy. I've moved them under folders based on which part of the UI they appear in: Dialog, GitHubPane and TeamExplorer.

Developer Documentation Added

I've added a few pieces of developer documentation explaining some of the new features:

https://github.com/github/VisualStudio/blob/refactor/mvvm/docs/developer/readme.md

Fixes #1105
Fixes #1341
Fixes #1357

The MVVM refactor (#1346) was originally based on the SSO feature branch and had a _lot_ of WIP etc commits. Rebased it off of #1353 and squashed into a single commit.
@grokys grokys changed the base branch from feature/sso to refactor/1352-remove-designtimestylehelper November 28, 2017 18:44
: base(exception.Message, innerException: exception)
{
Guard.ArgumentNotNull(exception, nameof(exception));

TwoFactorType = exception.TwoFactorType;
TwoFactorType = twoFactorType;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this extra constructor because the detection of the two-factor type is deep within Octokit's header parsing code. This allows us to easily override that in unit tests.

</DataTemplate>
</ContentControl.Resources>
</ContentControl>
<TextBlock Grid.Column="1"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to make this change because for some reason the old way of doing it was causing a StackOverflowException in WPF's layouting code. This change should produce equivalent results and is in fact simpler.

@grokys grokys changed the title WIP: MVVM-ize things MVVM-ize things Nov 29, 2017
ICompositionService cc)
{
this.serviceProvider = serviceProvider;
cc.SatisfyImportsOnce(this);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think importing ICompositionService or calling SatisfyImportsOnce is necessary. This should happen anyway after ViewViewModelFactory has been created.

I'm not sure AllowRecomposition = true is supported in Visual Studio's MEF implementation. I'm guessing this class some from somewhere else?

Copy link
Contributor Author

@grokys grokys Nov 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure - this class used to be ExportFactoryProvider and the code comes from there.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this query worth putting in another issue under "needs investigation" so that we can look into it after this PR gets merged?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put a breakpoint on the set method of Views and this property is indeed being set twice (with the same number of elements).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sguthals Done, see #1368.

@jcansdale
Copy link
Collaborator

When I click on the margin to add inline comment, visual Studio crashes with the following:

image
image

@meaghanlewis
Copy link
Contributor

meaghanlewis commented Nov 29, 2017

I noticed that when not signed into GitHub and trying to view a repository's pull requests that the view just spins forever:

expected
prompt login pr

actual
forever spinning pr

@meaghanlewis
Copy link
Contributor

meaghanlewis commented Nov 29, 2017

When checking out a PR branch, I can do so successfully but the view doesn't update from saying checkout test-pr-creation to local branch updated

expected
local branch up to date

actual
should say local branch up to date

And then im unable to view changes in solution or open file in solution
screen shot 2017-11-29 at 1 26 20 pm


</DataTemplate>
</Window.Resources>
</pfui:DialogWindow>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be </Window>?


<!-- Display ViewModel.Greeting in a red border with rounded corners -->
<Border Background="Red" CornerRadius="8">
<TextBlock Binding="{Binding Greeting}"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a />.

grokys and others added 12 commits December 1, 2017 10:12
This commit will automatically dispose of any preview PullRequestDetailViewModel object.
Keep CA happy by making AutoDispose static.
PullRequestDetailViewModel will be disposed in future commit.
Lightweight fix for PR detail view not refreshing
 Conflicts:
	src/GitHub.App/ViewModels/GitHubPane/PullRequestDetailViewModel.cs
@jcansdale
Copy link
Collaborator

image

Should we #pragma warning disable CA1004 to keep the build clean?

Copy link
Collaborator

@jcansdale jcansdale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good!

See comment about the CA1004 warning.

`IVSGitExt.ActiveRepositoriesChanged` isn't notified on the UI thread which breaks things. Switch to the main thread when refreshing due to a notification from this.

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!

cc.SatisfyImportsOnce(this);
}

[ImportMany(AllowRecomposition = true)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Visual Studio's MEF implementation is static (the graph is created once and cached). New exports/assemblies can't be added to the catalog on the fly. Having AllowRecomposition = true is confusing because recomposition is impossible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, well I'm fine with removing it! We should probably do it in a new PR though in case it causes problems.

/// <inheritdoc/>
public TViewModel CreateViewModel<TViewModel>() where TViewModel : IViewModel
{
return serviceProvider.ExportProvider.GetExport<TViewModel>().Value;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do the same as with CreateView and avoid any dependency on IGitHubServiceProvider.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! Lets try that in a separate PR too.

ICompositionService cc)
{
this.serviceProvider = serviceProvider;
cc.SatisfyImportsOnce(this);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this query worth putting in another issue under "needs investigation" so that we can look into it after this PR gets merged?

@@ -131,19 +132,40 @@
<Reference Include="WindowsBase" />
</ItemGroup>
<ItemGroup>
<Compile Include="Factories\ViewViewModelFactory.cs" />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming "ViewViewModelFactory.cs" is meant to have two "view"'s and isn't a typo. But thought it was worth asking ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's used for creating both views and view models. I did think it looked strange until I thought that MVVM stands for "model, view, view model" and so I decided to keep it.

{
history = new ReactiveList<IPanePageViewModel>();
history.BeforeItemsAdded.Subscribe(BeforeItemAdded);
history.CollectionChanged += CollectionChanged;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are BeforeItemAdded and CollectionChanged coming from?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NVM - I see them below.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants