Skip to content

Commit 10f72ff

Browse files
authored
feat(services): implement CPS-compatible project enumeration (#7)
* feat(services): implement CPS-compatible project enumeration Add interfaces and MSBuild query service for improved project handling: Interfaces: - ISolutionService for solution project enumeration - IManifestService for manifest loading/saving - IMsBuildQueryService for MSBuild property and target queries Implementation: - MsBuildQueryService with XML-based property and target detection - Special handling for TemplateProjectOutputGroup target detection - SolutionService now implements ISolutionService interface - ManifestService now implements IManifestService interface Model updates: - Add HasTemplateOutputGroup property to ProjectInfo Closes #3 * fix(views): use ServiceProvider.GlobalProvider for dialog service provider SVsServiceProvider is not retrievable via GetGlobalService, causing the Add buttons in AssetsView and DependenciesView to silently fail. Changed to use ServiceProvider.GlobalProvider which is always available. * fix(dialogs): define BoolToVisibilityConverter in XAML resources StaticResource is resolved at XAML parse time, so adding the converter in code-behind after InitializeComponent() fails. Moved the converter definition to XAML resources where it's available during parsing. * fix(dialogs): filter current project and improve dialog properties - Filter out the project containing the manifest being edited from the project picker (can't add a project as an asset to itself) - Add ManifestFilePath property to ManifestViewModel - Pass manifest path through AddAssetDialog to ProjectPickerDialog - Add HasMaximizeButton/HasMinimizeButton="False" to all dialogs for consistent VS-style dialog appearance * fix(dialogs): set proper VS window ownership for dialogs Add DialogHelper class that uses IVsUIShell.GetDialogOwnerHwnd() to properly parent dialogs to the Visual Studio main window. This ensures dialogs stay with VS when alt-tabbing and appear in front of VS. - Create DialogHelper with SetOwner and ShowDialogWithOwner methods - Update all views and dialogs to use DialogHelper.ShowDialogWithOwner - Add ThreadHelper.ThrowIfNotOnUIThread() calls to satisfy analyzer * chore: add logo and update manifest resources - Add logo.png to resources folder - Configure manifest with Icon and PreviewImage - Move LICENSE to resources\LICENSE in VSIX - Update csproj to include logo in VSIX package
1 parent 58685e1 commit 10f72ff

23 files changed

Lines changed: 377 additions & 55 deletions

resources/logo.png

1.17 MB
Loading

src/CodingWithCalvin.VsixManifestDesigner/CodingWithCalvin.VsixManifestDesigner.csproj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@
2626
<PackageReference Include="Microsoft.VisualStudio.SDK" Version="17.*" />
2727
</ItemGroup>
2828

29-
<!-- Include LICENSE in VSIX -->
29+
<!-- Include resources in VSIX -->
3030
<ItemGroup>
31-
<Content Include="..\..\LICENSE" Link="LICENSE">
31+
<Content Include="..\..\LICENSE" Link="resources\LICENSE">
32+
<IncludeInVSIX>true</IncludeInVSIX>
33+
</Content>
34+
<Content Include="..\..\resources\logo.png" Link="resources\logo.png">
3235
<IncludeInVSIX>true</IncludeInVSIX>
3336
</Content>
3437
</ItemGroup>

src/CodingWithCalvin.VsixManifestDesigner/Dialogs/AddAssetDialog.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
WindowStartupLocation="CenterOwner"
1212
ResizeMode="NoResize"
1313
ShowInTaskbar="False"
14+
HasMaximizeButton="False"
15+
HasMinimizeButton="False"
1416
Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}">
1517

1618
<platformUi:DialogWindow.Resources>

src/CodingWithCalvin.VsixManifestDesigner/Dialogs/AddAssetDialog.xaml.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace CodingWithCalvin.VsixManifestDesigner.Dialogs;
1313
public partial class AddAssetDialog : DialogWindow
1414
{
1515
private readonly IServiceProvider _serviceProvider;
16+
private readonly string? _manifestFilePath;
1617
private ProjectInfo? _selectedProject;
1718

1819
/// <summary>
@@ -24,7 +25,9 @@ public partial class AddAssetDialog : DialogWindow
2425
/// Initializes a new instance of the <see cref="AddAssetDialog"/> class for adding.
2526
/// </summary>
2627
/// <param name="serviceProvider">The service provider.</param>
27-
public AddAssetDialog(IServiceProvider serviceProvider) : this(serviceProvider, new Asset())
28+
/// <param name="manifestFilePath">The path to the manifest file being edited.</param>
29+
public AddAssetDialog(IServiceProvider serviceProvider, string? manifestFilePath = null)
30+
: this(serviceProvider, new Asset(), manifestFilePath)
2831
{
2932
}
3033

@@ -33,9 +36,11 @@ public partial class AddAssetDialog : DialogWindow
3336
/// </summary>
3437
/// <param name="serviceProvider">The service provider.</param>
3538
/// <param name="asset">The asset to edit.</param>
36-
public AddAssetDialog(IServiceProvider serviceProvider, Asset asset)
39+
/// <param name="manifestFilePath">The path to the manifest file being edited.</param>
40+
public AddAssetDialog(IServiceProvider serviceProvider, Asset asset, string? manifestFilePath = null)
3741
{
3842
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
43+
_manifestFilePath = manifestFilePath;
3944

4045
Asset = new Asset
4146
{
@@ -117,8 +122,10 @@ private void UpdateWarningVisibility()
117122

118123
private void BrowseProject_Click(object sender, RoutedEventArgs e)
119124
{
120-
var dialog = new ProjectPickerDialog(_serviceProvider);
121-
if (dialog.ShowDialog() == true && dialog.SelectedProject != null)
125+
Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread();
126+
127+
var dialog = new ProjectPickerDialog(_serviceProvider, _manifestFilePath);
128+
if (DialogHelper.ShowDialogWithOwner(dialog) == true && dialog.SelectedProject != null)
122129
{
123130
_selectedProject = dialog.SelectedProject;
124131
ProjectTextBox.Text = _selectedProject.Name;

src/CodingWithCalvin.VsixManifestDesigner/Dialogs/AddContentDialog.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
WindowStartupLocation="CenterOwner"
1212
ResizeMode="NoResize"
1313
ShowInTaskbar="False"
14+
HasMaximizeButton="False"
15+
HasMinimizeButton="False"
1416
Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}">
1517

1618
<platformUi:DialogWindow.Resources>

src/CodingWithCalvin.VsixManifestDesigner/Dialogs/AddDependencyDialog.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
WindowStartupLocation="CenterOwner"
1212
ResizeMode="NoResize"
1313
ShowInTaskbar="False"
14+
HasMaximizeButton="False"
15+
HasMinimizeButton="False"
1416
Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}">
1517

1618
<platformUi:DialogWindow.Resources>

src/CodingWithCalvin.VsixManifestDesigner/Dialogs/AddDependencyDialog.xaml.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace CodingWithCalvin.VsixManifestDesigner.Dialogs;
1313
public partial class AddDependencyDialog : DialogWindow
1414
{
1515
private readonly IServiceProvider _serviceProvider;
16+
private readonly string? _manifestFilePath;
1617
private ProjectInfo? _selectedProject;
1718

1819
/// <summary>
@@ -24,7 +25,9 @@ public partial class AddDependencyDialog : DialogWindow
2425
/// Initializes a new instance of the <see cref="AddDependencyDialog"/> class for adding.
2526
/// </summary>
2627
/// <param name="serviceProvider">The service provider.</param>
27-
public AddDependencyDialog(IServiceProvider serviceProvider) : this(serviceProvider, new Dependency())
28+
/// <param name="manifestFilePath">The path to the manifest file being edited.</param>
29+
public AddDependencyDialog(IServiceProvider serviceProvider, string? manifestFilePath = null)
30+
: this(serviceProvider, new Dependency(), manifestFilePath)
2831
{
2932
}
3033

@@ -33,9 +36,11 @@ public partial class AddDependencyDialog : DialogWindow
3336
/// </summary>
3437
/// <param name="serviceProvider">The service provider.</param>
3538
/// <param name="dependency">The dependency to edit.</param>
36-
public AddDependencyDialog(IServiceProvider serviceProvider, Dependency dependency)
39+
/// <param name="manifestFilePath">The path to the manifest file being edited.</param>
40+
public AddDependencyDialog(IServiceProvider serviceProvider, Dependency dependency, string? manifestFilePath = null)
3741
{
3842
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
43+
_manifestFilePath = manifestFilePath;
3944

4045
Dependency = new Dependency
4146
{
@@ -95,12 +100,14 @@ private void UpdateSourceVisibility()
95100

96101
private void BrowseProject_Click(object sender, RoutedEventArgs e)
97102
{
98-
var dialog = new ProjectPickerDialog(_serviceProvider)
103+
Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread();
104+
105+
var dialog = new ProjectPickerDialog(_serviceProvider, _manifestFilePath)
99106
{
100107
ShowOnlyVsixProjects = true
101108
};
102109

103-
if (dialog.ShowDialog() == true && dialog.SelectedProject != null)
110+
if (DialogHelper.ShowDialogWithOwner(dialog) == true && dialog.SelectedProject != null)
104111
{
105112
_selectedProject = dialog.SelectedProject;
106113
ProjectTextBox.Text = _selectedProject.Name;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Windows;
3+
using System.Windows.Interop;
4+
using Microsoft.VisualStudio.Shell;
5+
using Microsoft.VisualStudio.Shell.Interop;
6+
7+
namespace CodingWithCalvin.VsixManifestDesigner.Dialogs;
8+
9+
/// <summary>
10+
/// Helper class for dialog operations.
11+
/// </summary>
12+
internal static class DialogHelper
13+
{
14+
/// <summary>
15+
/// Sets the owner of a WPF window to the Visual Studio main window.
16+
/// </summary>
17+
/// <param name="window">The window to set the owner for.</param>
18+
public static void SetOwner(Window window)
19+
{
20+
ThreadHelper.ThrowIfNotOnUIThread();
21+
22+
var uiShell = Package.GetGlobalService(typeof(SVsUIShell)) as IVsUIShell;
23+
if (uiShell != null && uiShell.GetDialogOwnerHwnd(out var hwnd) == 0)
24+
{
25+
var helper = new WindowInteropHelper(window)
26+
{
27+
Owner = hwnd
28+
};
29+
}
30+
}
31+
32+
/// <summary>
33+
/// Shows a dialog with proper VS ownership.
34+
/// </summary>
35+
/// <param name="window">The window to show.</param>
36+
/// <returns>The dialog result.</returns>
37+
public static bool? ShowDialogWithOwner(Window window)
38+
{
39+
ThreadHelper.ThrowIfNotOnUIThread();
40+
SetOwner(window);
41+
return window.ShowDialog();
42+
}
43+
}

src/CodingWithCalvin.VsixManifestDesigner/Dialogs/ProjectPickerDialog.xaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
WindowStartupLocation="CenterOwner"
1212
ResizeMode="CanResize"
1313
ShowInTaskbar="False"
14+
HasMaximizeButton="False"
15+
HasMinimizeButton="False"
1416
Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}">
1517

1618
<platformUi:DialogWindow.Resources>
19+
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
1720
<Style TargetType="Label">
1821
<Setter Property="Foreground" Value="{DynamicResource {x:Static vs:VsBrushes.ToolWindowTextKey}}" />
1922
</Style>

src/CodingWithCalvin.VsixManifestDesigner/Dialogs/ProjectPickerDialog.xaml.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace CodingWithCalvin.VsixManifestDesigner.Dialogs;
1717
public partial class ProjectPickerDialog : DialogWindow
1818
{
1919
private readonly SolutionService _solutionService;
20+
private readonly string? _manifestFilePath;
2021

2122
/// <summary>
2223
/// Gets the selected project.
@@ -32,14 +33,14 @@ public partial class ProjectPickerDialog : DialogWindow
3233
/// Initializes a new instance of the <see cref="ProjectPickerDialog"/> class.
3334
/// </summary>
3435
/// <param name="serviceProvider">The service provider.</param>
35-
public ProjectPickerDialog(IServiceProvider serviceProvider)
36+
/// <param name="manifestFilePath">The path to the manifest being edited (to exclude its project).</param>
37+
public ProjectPickerDialog(IServiceProvider serviceProvider, string? manifestFilePath = null)
3638
{
3739
_solutionService = new SolutionService(serviceProvider);
40+
_manifestFilePath = manifestFilePath;
3841

3942
InitializeComponent();
4043

41-
Resources.Add("BoolToVisibilityConverter", new BooleanToVisibilityConverter());
42-
4344
Loaded += OnLoaded;
4445
}
4546

@@ -64,6 +65,24 @@ private async Task LoadProjectsAsync()
6465
projects = await _solutionService.GetProjectsAsync();
6566
}
6667

68+
// Filter out the project containing the manifest being edited
69+
if (!string.IsNullOrEmpty(_manifestFilePath))
70+
{
71+
var manifestDir = System.IO.Path.GetDirectoryName(_manifestFilePath);
72+
var filteredProjects = new List<ProjectInfo>();
73+
74+
foreach (var project in projects)
75+
{
76+
var projectDir = System.IO.Path.GetDirectoryName(project.FullPath);
77+
if (!string.Equals(projectDir, manifestDir, StringComparison.OrdinalIgnoreCase))
78+
{
79+
filteredProjects.Add(project);
80+
}
81+
}
82+
83+
projects = filteredProjects;
84+
}
85+
6786
ProjectListBox.ItemsSource = projects;
6887
}
6988

0 commit comments

Comments
 (0)