Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show Package Source Mapping Status in Details Pane #5166

Merged
merged 4 commits into from May 17, 2023

Conversation

donnie-msft
Copy link
Contributor

@donnie-msft donnie-msft commented May 12, 2023

Bug

Fixes: NuGet/Home#12586

Regression? Last working version:

Description

  • PM UI (project and solution levels) will now show a state indicating whether: Package Source Mapping is off; the package requires a mapping, or the package is mapped.

  • Shortcut to Settings page for mappings is shown beside the status

  • External changes to a nuget.config are handled. The cached mappings are refreshed, and bound property changed events are raised to update the XAML.

  • Accessibility Insights passed and manual Narrator testing and tab-order works as expected:
    image

  • PLOC testing done to ensure the strings display and wrap appropriately:
    image
    image

PR Checklist

@donnie-msft donnie-msft force-pushed the dev-donnie-msft-PMUIPackageSourceMappingStatus branch from ed4afa3 to 6f230fe Compare May 12, 2023 17:32
@donnie-msft donnie-msft marked this pull request as ready for review May 12, 2023 17:47
@donnie-msft donnie-msft requested a review from a team as a code owner May 12, 2023 17:47
Copy link
Contributor

@jebriede jebriede left a comment

Choose a reason for hiding this comment

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

Great work! I like how this is using MVVM for the new package source mapping section. The addition of the ViewModelBase will be useful for common ViewModel functionality. Left some comments, mostly relating to MVVM responsibilities.

{
internal INuGetUI UIController { get; }

private PackageSourceMappingActionViewModel(INuGetUI uiController)
Copy link
Contributor

Choose a reason for hiding this comment

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

The ViewModel should not take a direct dependency on the INuGetUI. Currently, the dependency on INuGetUI in this ViewModel enables this class to handle two things which can be handled in their appropriate layers:

  1. Providing the PackageSourceMappingControl access to the UIController. The Control then asks the UIController to launch the Options dialog which is a View concept so the ViewModel should not need to know about the View-specific helper class. The Control should take a direct dependency on the UIController instead of using the ViewModel as a pass-through.
  2. The UIController has the UIContext which holds on to the PackageSourceMapping instance. The ViewModel is holding on to the INuGetUI to gain access to the UIContext and in turn, the PackageSourceMapping instance. Instead, the ViewModel should take a direct dependency on the PackageSourceMapping model that should be supplied to it when the ViewModel is created.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Appreciate your diligence digging into the patterns used here. Just wanted to mention that I am only focusing on new code being respectful of MVVM, but am not refactoring any existing monolith UI logic to follow the pattern. Large-scale refactoring is out of scope for this PR.

The interaction with the View and ViewModel makes it so the View listens for changes to the Settings model directly[…]
The Control then asks the UIController to launch the Options dialog which is a View concept […]
Currently, the View is acting as the moderator between the PackageSourceMappingActionViewModel and the ISettings model[…]

The way I think about this interaction is how I think of Commands. Commands may be bound in the View (similar to attaching Click event handlers), but Command implementation is actually found in VMs, not the View (still a prominent pattern mentioned in recent MAUI docs, for example: Model-View-ViewModel | Microsoft Learn). Most commands interact with other VMs, and UIController is (partly) a VM. Unfortunately, our PackageManagerControl doesn't have its own VM which could host the same logic rather than in a Click event handler.

[...] the ViewModel should take a direct dependency on the PackageSourceMapping model [...]

UIController doesn't follow MVVM, but to me this is a mixed class with Model and ViewModel aspects. For instance, it knows how to create a (psuedo-ViewModel) class instance and pass that to a View which displays a Modal popup window. At the same time, it models properties like RecommendedCount which is something we'd expect to see in a VM.
In lieu of any significant refactoring to break apart PackageManagerControl's DataContext, I believe PackageSourceMapping is another component making up this model.

Copy link
Contributor

@jebriede jebriede May 16, 2023

Choose a reason for hiding this comment

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

Sorry for the misunderstanding. I am not suggesting we refactor the INugetUI / UIController. I agree that is out of scope for this PR and we don't need to touch that in order to make the changes I proposed. The changes I'm proposing should be fairly simple: Instead of passing the UIController as a dependency to the ViewModel, pass the objects that the ViewModel needs from the UIController. Looking carefully at the code, I don't see the ViewModel invokes anything directly on the UIController. So instead of depending on the container object for the actual dependencies the ViewModel needs, the ViewModel should depend directly on the objects it's getting from the UIController. Specifically, the ViewModel uses only the PackageSourceMapping which it gets from the UIContext from the UIController. The real dependency here is PackageSourceMapping so let's pass that in directly.

Comment on lines +186 to 189
private void Settings_SettingsChanged(object sender, EventArgs e)
{
_detailModel.PackageSourceMappingViewModel.SettingsChanged();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

The interaction with the View and ViewModel makes it so the View listens for changes to the Settings model directly, then invokes a method on the ViewModel to raise property change events for properties that the View is supposed to react to. Currently, the View is acting as the moderator between the PackageSourceMappingActionViewModel and the ISettings model which violates the MVVM pattern.

Instead, the ViewModel should have a dependency on the ISettings model and listen for changes by subscribing to the SettingsChanged event. When SettingsChanged is invoked by Settings, the ViewModel's private event handler will raise the necessary property changed events. The handler should be private though because only the ViewModel should invoke it or know when to invoke it. Since the View is listening for PropertyChanged events on the ViewModel (through data binding), the View will get updated when the property changed invocations happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Consolidating similar comments to this thread: #5166 (comment)

Copy link
Contributor

@jebriede jebriede May 16, 2023

Choose a reason for hiding this comment

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

In this code, the View (PackageManagerControl) listens directly to the Model (ISettings) and then instructs the ViewModel (PackageSourceMappingActionViewModel) to invoke the SettingsChanged which in turn tells the View to update. There are a few reasons this is not good:

  1. Since the View is directly responding to the Model's event, there's no reason for the View to then tell the ViewModel to fire a PropertyChanged event which just tells the View that the properties changed. The View already knows things changed. It doesn't need to tell another object to tell itself that the model changed.
  2. The proper relationship between the View, ViewModel, and Model is like this:

View -> ViewModel -> Model

The way this code is structured, the interaction is like this:

View -> Model
View -> ViewModel

The View should only interact with the ViewModel and not the model (ISettings) directly. The ViewModel should listen for changes to the ISettings and when they're found, fire the property changed notification to inform the View things changed.

I'm not suggesting we do any refactoring in the View to eliminate its dependency on the ISettings for other code. That's out of scope for this change. The View probably uses ISettings for other things and that's ok for now and not something we should fix in this feature PR. However, we can make some small changes in this PR to not continue that pattern. Simply by passing ISettings as a dependency for the ViewModel and interacting with the ViewModel from the View for this feature we will be on the right track. Also, we should definitely fix how the View tells the ViewModel to tell itself that things changed. That's a public API on the ViewModel that doesn't offer any value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this code, the View (PackageManagerControl) listens directly to the Model (ISettings)

Could you link to the line you're referring to? The comment is on a line accessing _detailModel which is not a View, and that's from within an event handler, Settings_SettingsChanged, which is also not attached to a View.
https://github.com/NuGet/NuGet.Client/blob/1ed4af017cf61be56b38583baf04a7f6c437e456/src/NuGet.Clients/NuGet.PackageManagement.UI/Xamls/PackageManagerControl.xaml.cs#LL188C13-L188C25

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you link to the line you're referring to?

Yep! It's just a few lines up in this PackageManagerControl.xaml.cs:

Settings.SettingsChanged += Settings_SettingsChanged;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this code, the View (PackageManagerControl) listens directly to the Model (ISettings)

Could you link to the line you're referring to?

Yep! It's just a few lines up in this PackageManagerControl.xaml.cs:

Settings.SettingsChanged += Settings_SettingsChanged;

Thanks for providing that, but neither Settings nor SettingsChanged nor the implementation of SettingsChanged are a View, and none of them manipulate a View.

Copy link
Contributor

@jebriede jebriede May 18, 2023

Choose a reason for hiding this comment

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

I didn't say that the Settings manipulates the View.

Settings is a Model. PackageManagerControl is a View. This PR has the the PackageManagerControl View listening directly to the Settings Model for changes. This is not correct. Furthermore, I pointed out code that is not providing any value above. This should have been fixed before this was merged. Can you please submit a follow-up PR addressing this comment? I'm happy to have a chat offline to clarify any confusion.

Copy link
Contributor

@jebriede jebriede May 19, 2023

Choose a reason for hiding this comment

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

@donnie-msft: I made the change to illustrate how the ViewModel (PackageSourceMappingActionViewModel) would listen to the Model (Settings) and update the View (PackageManagerControl) via the PropertyChanged notifications. This change eliminates the need for the PackageManagerControl to handle the Settings.SettingsChanged directly and now it relies on the ViewModel to tell it when things in the Settings updated which is what we want in MVVM interactions.

It also eliminates the public method on the ViewModel and relies on a private event handler instead, so the ViewModel can determine when it needs to raise its property changed notifications, not the View (PackageManagerControl).

Here's the PR, currently in draft: #5173

@donnie-msft donnie-msft force-pushed the dev-donnie-msft-PMUIPackageSourceMappingStatus branch from 6f230fe to 2b897ea Compare May 16, 2023 14:04
@donnie-msft donnie-msft requested a review from jebriede May 16, 2023 14:11
@donnie-msft donnie-msft enabled auto-merge (squash) May 16, 2023 14:11
@donnie-msft donnie-msft dismissed jebriede’s stale review May 16, 2023 14:17

pushed new commits

@donnie-msft donnie-msft requested a review from a team May 16, 2023 14:17
@donnie-msft donnie-msft force-pushed the dev-donnie-msft-PMUIPackageSourceMappingStatus branch from 2b897ea to 1ed4af0 Compare May 16, 2023 17:12
@donnie-msft donnie-msft force-pushed the dev-donnie-msft-PMUIPackageSourceMappingStatus branch from 1ed4af0 to 3f66805 Compare May 17, 2023 19:47
@erdembayar
Copy link
Contributor

erdembayar commented May 17, 2023

@donnie-msft
I tested this branch for onboarding a repository that has its own nuget.config file. However, it's creating the following entry in the default nuget.config file in AppData>>Roaming. Is this the expected behavior? I would expect it to be in the repository's nuget.config, not the default one, because mappings are usually repo specific, 2 different repos may have different mapping rules for same package Id.

  <packageSourceMapping>
    <packageSource key="nuget">
        <package pattern="NuGet1" />
      </packageSource>
  </packageSourceMapping>

@erdembayar
Copy link
Contributor

Also it accept 2 different entries for same source nuget. Is it expected?

image

@donnie-msft
Copy link
Contributor Author

@donnie-msft I tested this branch for onboarding a repository that has its own nuget.config file. However, it's creating the following entry in the default nuget.config file in AppData>>Roaming. Is this the expected behavior? I would expect it to be in the repository's nuget.config, not the default one, because mappings are usually repo specific, 2 different repos may have different mapping rules for same package Id.

  <packageSourceMapping>
    <packageSource key="nuget">
        <package pattern="NuGet1" />
      </packageSource>
  </packageSourceMapping>

This PR doesn't change how mappings are created. It's just a UI for status. Perhaps there's behavior there that's not expected. Do you have a <clear /> in your solution config?

@erdembayar
Copy link
Contributor

For me it's not clear "Requires a package source mapping.", it doesn't tell if I'm already onboarded to source mapping or not, if not how to onboard.
image

@donnie-msft
Copy link
Contributor Author

Also it accept 2 different entries for same source nuget. Is it expected?

image

You're testing VS Options, which is not what this PR modifies. Please test the PM UI changes for this PR.
(FYI, yes, that's known behavior. It will save 1 entry, but the UI isn't merging things together as you add them. There's other Options design changes needed, like NuGet/Home#12155)

@donnie-msft
Copy link
Contributor Author

For me it's not clear "Requires a package source mapping.", it doesn't tell if I'm already onboarded to source mapping or not, if not how to onboard. image

"Requires" implies you're onboarded. We don't require source mappings for restore unless you're onboarded.
There's a different informational message for not being onboarded:

<data name="Text_PackageMappingsDisabled" xml:space="preserve">
<value>Package source mapping is off.</value>
</data>

@donnie-msft donnie-msft merged commit 216a150 into dev May 17, 2023
10 of 15 checks passed
@donnie-msft donnie-msft deleted the dev-donnie-msft-PMUIPackageSourceMappingStatus branch May 17, 2023 22:01
@donnie-msft
Copy link
Contributor Author

Synced offline with @erdembayar and we added some more discussion points to the spec for improving nuget.config interaction/validation: NuGet/Home#12557

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants