microsoft / VSProjectSystem

Documentation for extending Visual Studio with new types of projects.
Other
313 stars 87 forks source link

Additional Debug Target is generated - not sure why. #189

Open dazinator opened 7 years ago

dazinator commented 7 years ago

I am using a IDynamicEnumValuesGenerator to provide enum values to display as targets in the debugger dropdown.

However, in addition to the values I am generating, I am also seeing a default value - which looks similar to the one generated for managed projects:

image

In the above image, I am generating the bottom 2 values, but I am not sure where the first value is coming from - which is the name of the project itself.

I know that managed project system generates a value like this, so I am wondering if perhaps this is making its way into my project system somehow.

My capabilities:


<ItemGroup>
    <ProjectCapability Include="DnnProjectSystem;" />
    <ProjectCapability Include="AssemblyReferences;ProjectReferences;" />
    <ProjectCapability Include="AllTargetOutputGroups;VisualStudioWellKnownOutputGroups;" />
    <ProjectCapability Include="ProjectConfigurationsDeclaredAsItems;" />
    <ProjectCapability Include="DeclaredSourceItems;UserSourceItems;CSharp;" />
    <ProjectCapability Include="SDKReferences;COMReferences;" />
    <ProjectCapability Include="SingleFileGenerators;" />
    <ProjectCapability Include="CPS;" />
    <ProjectCapability Include="ReferencesFolder"/>
    <!--<ProjectCapability Include="JavaScript;TypeScript;" />-->
  </ItemGroup>

My debugger rule:

<?xml version="1.0" encoding="utf-8"?>
<!--
    TODO: Reference the rule from the targets file using the PropertyPageSchema tag

    Example: Including the following in the targets file will add a new property page in the project properties dialog.
    <PropertyPageSchema Include="$(MSBuildThisFileDirectory)Rules\ProjectDebugger.xaml;">
      <Context>Project</Context>
    </PropertyPageSchema>

    For more information, please refer to the following blog post:
    http://blogs.msdn.com/b/vsproject/archive/2009/06/18/platform-extensibility-part-2.aspx
-->
<Rule
    Name="DnnWebsiteDebugger"
    DisplayName="Local Dnn Website"
    PageTemplate="Debugger"
    Description="Dnn Debugging Options"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns="http://schemas.microsoft.com/build/2009/properties">

    <Rule.DataSource>
        <DataSource Persistence="UserFileWithXamlDefaults"  HasConfigurationCondition="True"/>
    </Rule.DataSource>

    <!--<Rule.DataSource>
        <DataSource Persistence="ProjectFile" HasConfigurationCondition="False" SourceOfDefaultValue="AfterContext" />
    </Rule.DataSource>-->

    <!-- the command which appears in the debugger dropdown -->
    <Rule.Metadata>
        <sys:Guid x:Key="DebugTargetTypeCommandGuid">568ABDF7-D522-474D-9EED-34B5E5095BA5</sys:Guid>
        <sys:UInt32 x:Key="DebugTargetTypeCommandId">0x100</sys:UInt32>
        <sys:String x:Key="DebugTargetDropdownEnum">ActiveDebugProfile</sys:String>
    </Rule.Metadata>

    <DynamicEnumProperty Name="ActiveDebugProfile" DisplayName="Debug Target" EnumProvider="DnnDebugProfileProvider"
                       Description="Specifies the profile to use for debugging">
        <DynamicEnumProperty.DataSource>
            <DataSource Persistence="UserFile" HasConfigurationCondition="True" SourceOfDefaultValue="AfterContext" />
        </DynamicEnumProperty.DataSource>
    </DynamicEnumProperty>

</Rule>

My debug targets generator:


    [ExportDynamicEnumValuesProvider("DnnDebugProfileProvider")]
    [AppliesTo(MyUnconfiguredProject.UniqueCapability)]
    [Export(typeof(IDynamicDebugTargetsGenerator))]
    [ExportMetadata("Name", "DnnDebugProfileProvider")]
    internal class DnnDebugProfileProvider : ProjectValueDataSourceBase<IReadOnlyList<IEnumValue>>, IDynamicEnumValuesProvider, IDynamicDebugTargetsGenerator
    {

        private IReceivableSourceBlock<IProjectVersionedValue<IReadOnlyList<IEnumValue>>> _publicBlock;

        // Represents the link to the launch profiles
           private IDisposable _launchProfileProviderLink;

        // Represents the link to our source provider
         private IDisposable _debugProviderLink;

        [ImportingConstructor]
        public DnnDebugProfileProvider(UnconfiguredProject unconfiguredProject, ILaunchSettingsProvider launchSettingProvider)
            : base(unconfiguredProject.Services)
        {
            LaunchSettingProvider = launchSettingProvider;
        }

        private NamedIdentity _dataSourceKey = new NamedIdentity();
        public override NamedIdentity DataSourceKey
        {
            get { return _dataSourceKey; }
        }

        private int _dataSourceVersion;
        public override IComparable DataSourceVersion
        {
            get { return _dataSourceVersion; }
        }

        public override IReceivableSourceBlock<IProjectVersionedValue<IReadOnlyList<IEnumValue>>> SourceBlock
        {
            get
            {
                EnsureInitialized();
                return _publicBlock;
            }
        }

        private ILaunchSettingsProvider LaunchSettingProvider { get; }

        /// <summary>
        /// This provides access to the class which creates the list of debugger values..
        /// </summary>
        public Task<IDynamicEnumValuesGenerator> GetProviderAsync(IList<NameValuePair> options)
        {
            return Task.FromResult<IDynamicEnumValuesGenerator>(new DnnWebsiteValuesGenerator(LaunchSettingProvider));
        }

        protected override void Initialize()
        {
            var debugProfilesBlock = new TransformBlock<ILaunchSettings, IProjectVersionedValue<IReadOnlyList<IEnumValue>>>(
                update =>
                {
                    // Compute the new enum values from the profile provider
                    var generatedResult = DnnWebsiteValuesGenerator.GetEnumeratorEnumValues(update).ToImmutableList();
                    _dataSourceVersion++;
                    var dataSources = ImmutableDictionary<NamedIdentity, IComparable>.Empty.Add(DataSourceKey, DataSourceVersion);
                    return new ProjectVersionedValue<IReadOnlyList<IEnumValue>>(generatedResult, dataSources);
                });

            var broadcastBlock = new BroadcastBlock<IProjectVersionedValue<IReadOnlyList<IEnumValue>>>(b => b);

            _launchProfileProviderLink = LaunchSettingProvider.SourceBlock.LinkTo(
                debugProfilesBlock,
                linkOptions: new DataflowLinkOptions { PropagateCompletion = true });

            _debugProviderLink = debugProfilesBlock.LinkTo(broadcastBlock, new DataflowLinkOptions { PropagateCompletion = true });

            _publicBlock = broadcastBlock.SafePublicize();
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_launchProfileProviderLink != null)
                {
                    _launchProfileProviderLink.Dispose();
                    _launchProfileProviderLink = null;
                }

                if (_debugProviderLink != null)
                {
                    _debugProviderLink.Dispose();
                    _debugProviderLink = null;
                }
            }

            base.Dispose(disposing);
        }
    }

and the enum values generator


 dynamic enum values.
    /// </summary>
    public class DnnWebsiteValuesGenerator : IDynamicEnumValuesGenerator
    {
        private ILaunchSettingsProvider launchSettingProvider;

        /// <summary>
        /// The listed values.
        /// </summary>
        private AsyncLazy<ICollection<IEnumValue>> listedValues;

        public DnnWebsiteValuesGenerator(ILaunchSettingsProvider launchSettingProvider)
        {

            listedValues = new AsyncLazy<ICollection<IEnumValue>>(delegate
            {
                var curSnapshot = launchSettingProvider.CurrentSnapshot;
                if (curSnapshot != null)
                {
                    return Task.FromResult(GetEnumeratorEnumValues(curSnapshot));
                }

                ICollection<IEnumValue> emptyCollection = new List<IEnumValue>();
                return Task.FromResult(emptyCollection);
            });

            this.launchSettingProvider = launchSettingProvider;
        }

        internal static ICollection<IEnumValue> GetEnumeratorEnumValues(ILaunchSettings profiles)
        {
            Collection<IEnumValue> result = new Collection<IEnumValue>(
            (
                from profile in profiles.Profiles
                let value = new EnumValue { Name = profile.Name, DisplayName = profile.Name }
                select ((IEnumValue)new PageEnumValue(value))).ToList()
            );

            return result;

        }

        /// <summary>
        /// Gets whether the dropdown property UI should allow users to type in custom strings
        /// which will be validated by <see cref="TryCreateEnumValueAsync"/>.
        /// </summary>
        public bool AllowCustomValues
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// The list of values for this property that should be displayed to the user as common options.
        /// It may not be a comprehensive list of all admissible values however.
        /// </summary>
        /// <seealso cref="AllowCustomValues"/>
        /// <seealso cref="TryCreateEnumValueAsync"/>
        public async Task<ICollection<IEnumValue>> GetListedValuesAsync()
        {
            return await listedValues.GetValueAsync().ConfigureAwait(true);
        }

        /// <summary>
        /// Tries to find or create an <see cref="IEnumValue"/> based on some user supplied string.
        /// </summary>
        /// <param name="userSuppliedValue">The string entered by the user in the property page UI.</param>
        /// <returns>
        /// An instance of <see cref="IEnumValue"/> if the <paramref name="userSuppliedValue"/> was successfully used
        /// to generate or retrieve an appropriate matching <see cref="IEnumValue"/>.
        /// A task whose result is <c>null</c> otherwise.
        /// </returns>
        /// <remarks>
        /// If <see cref="AllowCustomValues"/> is false, this method is expected to return a task with a <c>null</c> result
        /// unless the <paramref name="userSuppliedValue"/> matches a value in <see cref="GetListedValuesAsync"/>.
        /// A new instance of an <see cref="IEnumValue"/> for a value
        /// that was previously included in <see cref="GetListedValuesAsync"/> may be returned.
        /// </remarks>
        public async Task<IEnumValue> TryCreateEnumValueAsync(string userSuppliedValue)
        {
            return (await listedValues.GetValueAsync().ConfigureAwait(true))
           .FirstOrDefault(v => LaunchProfile.IsSameProfileName(v.Name, userSuppliedValue));
        }
    }

I have confirmed that in all cases, I am only ever returning 2 enum values, so i can't see where this third entry is coming from.

If you attempt to debug with it you get a standard managed project system error message too:

image

dazinator commented 7 years ago

I noticed that, in the rule file for the debugger, there is some metadata that hooks up the enum provider to an existing command - I am wondering - could some other rules coming from elsewhere be doing the same thing to add in this additional value? The trouble is I am not sure how I can track them down if that is the case.

 <!-- the command which appears in the debugger dropdown -->
    <Rule.Metadata>
        <sys:Guid x:Key="DebugTargetTypeCommandGuid">568ABDF7-D522-474D-9EED-34B5E5095BA5</sys:Guid>
        <sys:UInt32 x:Key="DebugTargetTypeCommandId">0x100</sys:UInt32>
        <sys:String x:Key="DebugTargetDropdownEnum">ActiveDebugProfile</sys:String>
    </Rule.Metadata>
adrianvmsft commented 7 years ago

Could you check the value you have specified in the .props file as DebuggerFlavor? It should point to your Debugger:

Based on the snippets you posted above, it should be something like:

<DebuggerFlavor>DnnWebsiteDebugger</DebuggerFlavor>
dazinator commented 7 years ago

Sure - yes it is:

<DebuggerFlavor>DnnWebsiteDebugger</DebuggerFlavor>

In addition, if I select a debug target that I am generating: image

I can launch my custom debugger and step through the code and it appears to work fine.

If I select the debug target that is there by default then my custom debugger doesn't get invoked and I see the standard error about the project being a library project:

image

adrianvmsft commented 7 years ago

Perhaps it got persisted initially to the project or the .user file before you added your debugger? If yes, could you try removing it to see if that fixes it?

dazinator commented 7 years ago

If I remove this import from my project file:


  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

Then the entry disappears..

So seems this is automatically added when these targets are included..

dazinator commented 7 years ago

Right now, Microsoft.CSharp.targets are imported in my project file, and then my project system targets and props are added via a nuget package install. I wonder if removing the Microsoft.CSharp.targets and then adding them after the nuget install is finished will solve this (or importing Microsoft.CSharp.targets them from my custom targets instead) - I'll give that a go!

dazinator commented 7 years ago

Perhaps it got persisted initially to the project or the .user file before you added your debugger? If yes, could you try removing it to see if that fixes it?

I can't seem to find a user file anywhere after creating the project. I can't see any additional entries in the project file. Removing the import of Microsoft.CSharp.targets gets rid of the entry.

I'm experimenting with the order in which targets are imported, thinking perhaps importing Microsoft.CSharp.targets last, may mean it wont add the value anymore (i.e perhaps a default target is only generated if there isn't allready any values generated).

However this seems to be very fragile. I have taken the CSharp import out of the project file, and put it in my CustomProject targets file instead, but now when I create a new project I get this error:

image

Not very helpful error! Any ideas how I see more information about why project creation failed?

dazinator commented 7 years ago

Here is my somewhat educated guess over what is happening.

I am importing Microsoft.CSharp.targets

This is adding some capabilities - causing a managed CPS extension to be loaded for the debugger.

The managed extension is probably implementing IDynamicDebugTargetsGenerator and is running first, and creating a default debug target because if there are 0 debug targets this will lead to a poor experience if the user tries to debug.

My extension IDynamicDebugTargetsGenerator is being loaded after the managed one, and adding my own debug targets.

If this is right, all I need is to get my IDynamicDebugTargetsGenerator to run before the managed one..

How do I do that? I guess there must be an order attribute or something i can use with my export.. i'll try that.

adrianvmsft commented 7 years ago

Well, the roslyn project system is open source so you can search directly in the source to see.

So I found DebugProfileDebugTargetGenerator:

which gets activated on the "LaunchProfiles" capability. So you could try removing that capability from your targets:

<ProjectCapability Remove="LaunchProfiles" />

Now, because your project type imports CSharp targets, you will get a mixed set of features. You may also need clean up some of the rules that Project System Extensibility generated in your project as in Visual Studio 2017, C# targets uses a newer version.

Use

msbuild /pp:pp.txt

on one of your project to get a full picture of everything that gets included.

dazinator commented 7 years ago

Genius. I forgot you could remove capabilities as well as add them.

Also that msbuild command looks invaluable.

Thank you, much appreciated.

dazinator commented 7 years ago

@adrianvmsft - damn. So even though I am removing this capability, this additional debug target is still appearing. Yet if I don't import csharp targets at all then it disappears.

I don't know enough about how capabilities changing works - but I am assuming that importing csharp targets causes Roslyn capabilities to be added, and CPS extensions get loaded straight away - later when I remove those capabilities. it's too late, as CPS has already loaded the parts. Could this be what I am seeing?

dazinator commented 7 years ago

Not sure why this was closed.. Can you re-open as this is still an issue for me!

adrianvmsft commented 7 years ago

I took a look at the dotnet project system, it looks like this capability only gets included by this target file.

Could you try to comment out that line on your local machine to see if it fixes your issue?

If that is the cause, you should file an issue on the dotnet project system repo, as they own those capabilities/targets.

Thanks, Adrian

aienabled commented 7 years ago

I followed the advice of @adrianvmsft and it indeed work properly. It still display "Start" in the profiles list (I've deleted .csproj.user file), but it's not selectable and don't have an icon (see the attached screenshot) so it's not a problem at all. debuggers

Alas, <ProjectCapability Remove="LaunchProfiles" /> doesn't work. What worked for me is adding this block to my targets file:

<PropertyGroup>
    <DefineCommonManagedCapabilities>false</DefineCommonManagedCapabilities>
</PropertyGroup>

And then redefining there the common managed capabilities by copying block of code from target file with commented out <ProjectCapability Include="LaunchProfiles" />.

dazinator commented 7 years ago

@aienabled nice discovery! I will try the same tactic :-) thank you for sharing.

CartBlanche commented 2 years ago

@dazinator I'm also trying to customise Debug Targets for our IoT device list, but finding it hard to find documentation on how to get it all working. Are you able to point me in the right direction? I have the IDynamicEnumValuesGenerator and also the IDynamicEnumValuesProvider respective classes set up and also the rule updated to similar to what you have, but my list never appears in the IDE. Any pointers as MS documentation seems to be lacking and there doesn't seem to be a sample anywhere on how to do this.