I am trying to create a Visual Studio extension that will open a document window containing a custom control of mine that hosts an editor pane. I can get the document to load, the correct content type loads with my custom content extensions (classifier, quick info), a window with my custom control showing the editor pane loads, but the text cannot be modified. I can select and highlight text, but no keys work, no commands, and no mouse input other than text selection.
The following is what I have in my Package class:
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[ProvideEditorExtension(typeof(MyCustomEditorFactory), ".cust")]
[Guid(MyCustomExtensionsPackage.PackageGuidString)]
public sealed class MyCustomExtensionsPackage : AsyncPackage
{
public const string PackageGuidString = "ec2c4646-d0cc-42c6-b0a6-d0ff3e318cef";
#region Package Members
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
RegisterEditorFactory(new MyCustomEditorFactory(this));
}
#endregion
}
I have verified that the RegisterEditorFactory method is called, and that my editor factory is called when files with the .cust extension are opened.
The following is my editor factory:
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;
using System;
using System.ComponentModel.Composition;
using System.Runtime.InteropServices;
[ComVisible(true)]
[Guid(EditorFactoryGuidString)]
public sealed class MyCustomEditorFactory : IVsEditorFactory
{
public const string EditorFactoryGuidString = "38AD95BA-8891-46A2-A5EA-25F5F36EEAE0";
private MyCustomExtensionsPackage _package;
private Microsoft.VisualStudio.OLE.Interop.IServiceProvider _vsServiceProvider;
[Import]
public IContentTypeRegistryService ContentTypeRegistry { get; set; }
[Import]
public ITextEditorFactoryService TextEditorFactory { get; set; }
public MyCustomEditorFactory(MyCustomExtensionsPackage package)
{
_package = package;
}
public int SetSite(Microsoft.VisualStudio.OLE.Interop.IServiceProvider psp)
{
_vsServiceProvider = psp;
return (VSConstants.S_OK);
}
public int Close()
{
return (VSConstants.S_OK);
}
public int MapLogicalView(ref Guid rguidLogicalView, out string pbstrPhysicalView)
{
pbstrPhysicalView = null;
return (VSConstants.LOGVIEWID_Primary == rguidLogicalView ? VSConstants.S_OK : VSConstants.E_NOTIMPL);
}
public int CreateEditorInstance(uint grfCreateDoc, string pszMkDocument, string pszPhysicalView, IVsHierarchy pvHier, uint itemid, IntPtr punkDocDataExisting, out IntPtr ppunkDocView, out IntPtr ppunkDocData, out string pbstrEditorCaption, out Guid pguidCmdUI, out int pgrfCDW)
{
ThreadHelper.ThrowIfNotOnUIThread();
ppunkDocView = IntPtr.Zero;
ppunkDocData = IntPtr.Zero;
pbstrEditorCaption = string.Empty;
pguidCmdUI = VSConstants.GUID_TextEditorFactory;
pgrfCDW = 0;
int retVal = VSConstants.E_FAIL;
if ((grfCreateDoc & (VSConstants.CEF_OPENFILE | VSConstants.CEF_SILENT)) != 0)
{
IVsTextLines textBuffer = null;
if (punkDocDataExisting == IntPtr.Zero)
{
IComponentModel mef = _package.GetService<SComponentModel, IComponentModel>();
mef.DefaultCompositionService.SatisfyImportsOnce(this);
IVsEditorAdaptersFactoryService eafs = mef.GetService<IVsEditorAdaptersFactoryService>();
textBuffer = eafs.CreateVsTextBufferAdapter(_vsServiceProvider, ContentTypeRegistry.GetContentType("CUST")) as IVsTextLines;
string fileText = System.IO.File.ReadAllText(pszMkDocument);
textBuffer.InitializeContent(fileText, fileText.Length);
string[] roles = new string[]
{
PredefinedTextViewRoles.Analyzable,
PredefinedTextViewRoles.Editable,
PredefinedTextViewRoles.Interactive,
PredefinedTextViewRoles.Document,
PredefinedTextViewRoles.PrimaryDocument
};
IWpfTextView dataView = TextEditorFactory.CreateTextView(eafs.GetDataBuffer(textBuffer), TextEditorFactory.CreateTextViewRoleSet(roles));
dataView.Options.SetOptionValue(DefaultTextViewHostOptions.LineNumberMarginName, true);
dataView.Options.SetOptionValue(DefaultTextViewHostOptions.ShowCaretPositionOptionName, true);
dataView.Options.SetOptionValue(DefaultTextViewHostOptions.ChangeTrackingName, true);
dataView.Options.SetOptionValue(DefaultTextViewOptions.ViewProhibitUserInputName, false);
IWpfTextViewHost wpfHost = TextEditorFactory.CreateTextViewHost(dataView, false);
MyCustomEditor editor = new MyCustomEditor(wpfHost);
ppunkDocData = Marshal.GetIUnknownForObject(textBuffer);
ppunkDocView = Marshal.GetIUnknownForObject(editor);
retVal = VSConstants.S_OK;
}
else
{
//code for document already open
}
else
{
retVal = VSConstants.E_INVALIDARG;
}
}
return (retVal);
}
}
There's a lot to unpack there, but ultimately I'm just
Creating an IVsTextBuffer from an IVsEditorAdaptersFactoryService
Loading the contents of the text buffer
Creating an IWpfTextView with the IVsTextBuffer from a ITextEditorFactoryService
Creating an IWpfTextViewHost with the IWpfTextView from the same ITextEditorFactoryService
Creating my my custom window pane and passing in the IWpfTextViewHost
Returning the IVsTextBuffer as the document data and my custom window pane as the document view
My custom window pane code is simply:
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Text.Editor;
using System.Runtime.InteropServices;
[ComVisible(true)]
public sealed class MyCustomEditor : WindowPane
{
private IWpfTextViewHost _wpfHost;
public MyCustomEditor(IWpfTextViewHost wpfHost)
{
_wpfHost = wpfHost;
Content = new MyCustomEditorControl(wpfHost.HostControl);
}
}
I've omitted the code for the MyCustomEditorControl for the sake of brevity, but just assume it's a simple WPF UserControl with a ContentPresenter. The constructor just sets Content property of the ContentPresenter with the wpfHost.HostControl that's passed in.
All of this works insofar as displaying the contents of a file, but I just can't edit anything. It's like the input bindings aren't wired up or enabled, but I can't find what properties to set or enable on which objects, and the documentation is rather poor on this topic. I've gone through the Visual Studio extensions walkthroughs on MSDN, but it seems to be a mish-mash of pre-WPF and post-WPF APIs with no clear guidance on what the authoritative approach is.
Admittedly, I may be naive in assuming that the IWpfTextView would automatically handle user input, but in my defense it's created from an ITextEditorFactoryService and having "TextEditor" in the name carries certain implications.
I'm going to go through a rather exhaustive list of things I've tried and checked to hopefully help narrow down the possibilities or where my mistake is:
After the document window is open, I've checked the Running Document Table and there is an entry for it that is automatically created after CreateEditorInstance exits. It links to the data buffer that's returned, the correct file path, and has an edit lock but no read lock. I've inspected the RDT when C# files are opened in a normal editor window and those have read and write locks. I tried manually setting a read lock on the RDT as well, the counter increments, but that doesn't seem to make a difference.
I've inspected the return result from ITextBuffer.CheckEditAccess on the data buffer and it returns true so the text buffer is reporting it's editable.
The textview roles DO have an effect on the final rendered IWpfTextViewHost. If I add or omit roles, the appearance and functionality changes. For example, adding or removing "ZOOMABLE" adds or removes the dropdown box with the control zoom level. However, "EDITABLE" has no effect on editability.
The editor options DO have an effect. For example, adding or removing the line number margin options does show/hide the line numbers. However, ViewProhibitUserInputName has no effect on editability.
I don't create a code window with CreateVsCodeWindowAdapter because the resulting IWpfTextViewHost that can be retrieved from GetWpfTextViewHost is already parented to another control. If I disconnect it with (IWpfTextViewHost.HostControl.Parent as Border).Child = null, then the contents of IWpfTextViewHost.HostControl gets disposed of for some reason. The IWpfTextViewHost.HostControl loads and it displays a window with margins and scrollbars, but the content is blank and if I click into the left margin of the content pane then Visual Studio throws an ObjectDisposedException. Please note I'm not nulling out the host control itself or its contents, I'm only telling its existing parent it no longer has a child.
I've inspected the editor GUID property of the IVsWindowFrame that opens my document and it's set to the value returned in the pguidCmdUI parameter, which I believe is correct.
I'm not certain if the value being set for the pguidCmdUI parameter in the editor factory CreateEditorInstance is correct. I'm aware it's used for command routing, which might explain why I can't type or use the mouse, but I'm not sure what the correct value should be. I read somewhere it's supposed to be the GUID of the factory that created the editor, so I set it to the text editor factory GUID since that's what created the IWpfTextView. I tried setting it to my editor factory GUID at the top of the file, as well as typeof(IWpfTextView).GUID, Guid.Empty, and assorted other GUIDs, but there's no change.
If I pass my IWpfTextView into IVsEditorAdapterFactoryService.GetViewAdapter, I get back null.
If I create a IVsTextView from IVsEditorAdapterFactoryService.CreateVsTextViewAdapter, I can have both an IVsTextView and an IWpfTextView, but the two don't know about each other and I don't know see any way to map them. I don't even know if they need to be mapped, or if IVsTextView needs to exist or is just the legacy text view interface.
When I inspect the created IVsTextView, it's base type is a SimpleTextViewWindow that is an undocumented class that's internal to Microsoft's Visual Studio implementation, it has WpfTextView and WpfTextViewHost properties, but they're both null (or throw an exception that they can't be read) and are unsettable. Microsoft obviously has some internal voodoo magic to map IVsTextViews to WpfTextViews in their own code, but I don't know what it is.
I'm aware of other code samples that create an IVsInvisibleEditor and get the data buffer from that, but that seems kind of hacky. I mean, it technically works and no one seems to know of any alternative because the documentation in this area is so poor, so I'm not judging or criticizing, it just seems like that shouldn't be necessary. It's my (possibly incorrect) understanding that an invisible editor is a virtual editor that is created for documents that are opened in memory but not hosted in a window, but I'm hosting mine in a window. Additionally all of the working code samples I could find are all copy/pasting from the same Microsoft VSIX code example. In that example, the virtual editor is created in a tool window that's created under the assumption that a document is already open in an existing editor window. So that tool window is creating a virtual editor side-by-side to a physical one for a tool window. If I follow the same invisible editor approach, then I'm effectively creating a virtual editor in memory and then a second physical document editor window. So basically, I'm creating two editors for one document. That doesn't seem right. Again, it technically works, but it seems off.
If the expectation is that we have to implement IOleCommandTarget on our editor window and manually handle every single keystroke, shortcut, and mouse button and manipulate the underlying text buffer directly, then that is going to be the most depressing thing ever. That would mean they tout being able to use WPF, but then give us a document view without a WPF control that natively handles text editing even though they exist in the framework.
Sorry for the ridiculously long post, but I wanted to provide as much information as I possibly could about where I'm at and what I've tried.
Regarding CreateVsCodeWindowAdapter, see the "official" explanation how to make it work: https://developercommunity.visualstudio.com/t/projectionbuffertutorial-gives-error-in-dev16/498617
I have created a boolean algebraic simplifier. It simplifies expressions and I am content with it. However, I am trying to add a feature that allows users to check if two expressions are equivalent. For this I have created a new form that allows the user to input two expression by clicking buttons. To do this, I thought it best to simplify both expressions and then compare the two for equivalency. As I have got lots of subroutines and code that works for simplification in another form, I thought making the form a child form of the form with the code in would allow me to call the subroutines instead of copying them onto the form. I have made these protected in the parent form. I have inherited like so:
public partial class Expression_Equivalency_Form : Expression_Simplifier
However, when I click onto the form designer, this error appears and I cannot view the graphical interface of the form:
"Could not find file File Path"
The file is in the debug folder which is within the bin folder within the folder containing the program and is recongised in the parent class. The file is read from and appeneded by the parent form without issue. I have tried to research this but have been unable to find a solution. Does anyone know one?
I have read to the file and appended to it. I have also used the following code to remove any blank lines from my text file:
File.WriteAllLines("PreviousExpressionInputs.txt",
File.ReadAllLines("PreviousExpressionInputs.txt").Where(l => !string.IsNullOrWhiteSpace(l)));
Code that writes to the file:
using (BinaryWriter Writer = new BinaryWriter(File.Open("PreviousExpressionInputs.txt",
FileMode.Append)))
{
Writer.Write(expressionandanswertowritetotextfile);
}
Code that reads from the file:
foreach (string line in File.ReadLines("PreviousExpressionInputs.txt"))
{
try
{
LinesInFile.Add(line);
}
catch (Exception)
{
}
}
Consider following facts:
When you open a form in design mode, the constructor of its base class will run.
When you look for a relative file name, the path will be resolved relative to the current working directory of the application.
When the form is in design mode, the current application is Visual Studio and its working directory is where the devenv.exe is located.
It describes why you cannot find your text files. Because you have some code in the constructor of your base form(or fir example load event handler of the base form) which looks for the file and since the filename is relative, its looking for the file in the Visual Studio working directory and could not find file.
How to prevent the problem? Check DesignMode property to prevent running the code:
public partial class MyBaseForm : Form
{
public MyBaseForm()
{
InitializeComponent();
}
private void MyBaseForm_Load(object sender, EventArgs e)
{
MessageBox.Show("This will show both in run-time and design time.");
if (!DesignMode)
MessageBox.Show("This will show just in run-time");
}
}
Create the derived form and open it in designer to see what happens:
public partial class Form1 : MyBaseForm
{
public Form1()
{
InitializeComponent();
}
}
To learn more about how designer works take a look at this post.
The .NET Shell extension framework called SharpShell is great; I've developed a right-click file Shell ContextMenu "quite easily" that works selecting both files and directories.
Now I would like to develop a Shell ContextMenu by righ-clicking on an empty space (that is, on the Desktop or on a white spot while I'm inside a folder).
Is it possible do that still using SharpShell? Or do I need to move to a different solution?... and in 2nd case... what do you suggest?
Thanks
The two solutions presented below work, but in the meantime I have found that there is an easier solution that is actually already used in the samples that come with SharpShell.
See the CopyDirectoryLocationHandler class as an example of a context menu handler that is registered for the directory background (and the desktop):
[ComVisible(true)]
[COMServerAssociation(AssociationType.Class, #"Directory\Background")]
public class CopyDirectoryLocationHandler : SharpContextMenu
{
// ...
}
If you want the handler to only handle clicks on the desktop background, use this code instead:
[ComVisible(true)]
[COMServerAssociation(AssociationType.Class, #"DesktopBackground")]
public class CopyDirectoryLocationHandler : SharpContextMenu
{
// ...
}
Old obsolete answer:
You can use SharpShell for this purpose without problem. There are two possible approaches:
Register the Shell Extension to handle the folder background
yourself
or
Modify SharpShell to handle the registration of the
extension for the folder background for you.
Register the Shell Extension to handle the folder background yourself
Your shell extension is a COM server and as such is identified to the system via a GUID. This GUID is then used at places in the registry to register the COM extension for different purposes. When we manually want to register the extension for a purpose such as extending the context menu for folder backgrounds, it is best when our extension has a fixed GUID.
Currently your class looks like this:
[ComVisible(true)]
[COMServerAssociation(AssociationType.Directory)]
public class MyContextMenuExtension : SharpContextMenu
{
When compiling, the compiler will automatically generate a GUID to use for that class. But we can specify a specific one to use like this:
[Guid("A75AFD0D-4A63-41E3-AAAA-AD08A574B8B0")]
[ComVisible(true)]
[COMServerAssociation(AssociationType.Directory)]
public class MyContextMenuExtension : SharpContextMenu
{
Do not use the same GUID as shown here but create your own unique one in Visual Studio via Menu Tools > Create GUID. Use a different GUID for every shell extension you write.
Then recompile the dll and install and register it again (using regasm or the SharpShell Server Manager tool.
Then create a text file named "registry.reg" with the following content (use your own specific GUID). Instead of "MyContextMenuExtension" specify the name of your extension.
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\MyContextMenuExtension]
#="{A75AFD0D-4A63-41E3-AAAA-AD08A574B8B0}"
Install the "registry.reg" file by double clicking. Your extension should now be active when you open the context menu for a folder background or the Desktop.
Instead of using the *.reg file, you can also make the changes manually using registry editor or if you have an installer instruct the installer to make those registry changes.
Modify SharpShell to handle the registration of the extension for the folder background for you
Make the following changes to the SharpShell source code:
In the file AssociationType.cs add a new enum value to the AssociationType enumeration:
/// <summary>
/// Create an association to the unknown files class.
/// </summary>
UnknownFiles,
/// <summary>
/// Create an association to the background of folders and the desktop
/// </summary>
DirectoryBackground
In the file ServerRegistrationManager.cs add a new private string constant:
/// <summary>
/// The 'directory' special class.
/// </summary>
private const string SpecialClass_Directory = #"Directory";
/// <summary>
/// The 'directory background' special class.
/// </summary>
private const string SpecialClass_DirectoryBackground = #"Directory\Background";
Also in the file ServerRegistrationManager.cs in the method CreateClassNamesForAssociations in the big switch statement add a new case like this:
case AssociationType.Directory:
// Return the directory class.
return new[] { SpecialClass_Directory };
case AssociationType.DirectoryBackground:
// Return the directory background class.
return new[] { SpecialClass_DirectoryBackground };
Finally you only have to tell your own extension class to use this new enumeration value:
[Guid("A75AFD0D-4A63-41E3-AAAA-AD08A574B8B0")]
[ComVisible(true)]
[COMServerAssociation(AssociationType.Directory)]
[COMServerAssociation(AssociationType.DirectoryBackground)]
public class MyContextMenuExtension : SharpContextMenu
{
I have used SharpShell some time ago, forgotten it since then (because it works flawlessly). I have used it on files and folders, so your question intrigued me. A little research on the tool led me to the answer No(unfortunately).
The binding is done through the com server associations on SharpShell. And by looking at the documentation of the com server associations I am not seeing the way to your desired functionality.
PS: I encourage you to leave a comment on the documentation page, or contact directly with the author of the library. He seems to be really helpful(I've contacted him before).
I have a WinForm project that contains several UserControls. This WinForm project has a reference to an assembly (lets call it lib.dll) that is created from another project (Class Library) that exists in a different solution.
Now, several of the UserControls make calls into lib.dll that return values from the app.config file. At runtime lib.dll works fine and returns the necessary data but at design time, I am getting an exception from lib.dll because the app.config sections are NULL (the exceptions are by design).
Now I could go through each control and wrap any code that calls into lib with
if(!DesignMode) { //code }
But that is a lot of controls to go and apply that to. Is there something I can do globally that would be more elegant then testing the DesignMode property?
Edit
In response to the two comments left below: the solutions provided don't appear to work. The assembly that is causing me a problem lives in the same directory as the app.config. The general directory structure looks like this
References Folder
Configurations (Folder)
appsettings.config
app.config
lib.dll
app.config pulls in several other config files (appsettings, cnx strings, etc) which reside in the Configurations directory. In the case of my exception the value I am trying to get resides in one of these ancillary config files that is referenced by app.config.
This is an interesting question. A solution could be to create in lib.dll a static class like this one :
public static class Config
{
private static readonly _param1;
static Config()
{
_param1 = ConfigurationManager.AppSettings["Param1"] ?? "Your default value";
}
public static string Param1
{
get { return _param1; }
}
}
Then, in your code, insted of writing ConfigurationManager.AppSettings["Param1"], you will use Config.Param1. So you won't need to test the property DesignMode.
There are so many ways to do this, IMHO.
One thought that immedidately comes to mind would be to use an inheritance-based approach for the user controls in question? That way, in the base class, you can put that if (DesignMode) check in, and do the correct branching from there.
// if i were to visualizeyour lib.dll data initializer call like this:
class BaseUserControl
{
// i'm guessing that you initialize the data somehow...
void InitializeData()
{
if (!DesignMode)
{
InitializeDataLocal();
}
}
protected virtual InitializeDataLocal()
{
// whatever base behavior you want should go here.
}
}
// in the derived classes, just put the code you currently have for
// fetching the data from lib.dll here...
class UserControl : BaseUserControl
{
protected override InitializeDataLocal()
{
// fetch from lib.dll...
// optionally invoke some base behavior as well,
// if you need to...
base.InitializeDataLocal();
}
}
I have a very simple class
public class Preferences
{
public bool RepeatInfinite { get; set; }
public int RepeatCount { get; set; }
}
If I put this class in another assembly it shows up in the Settings tab when you browse for the type. If I however put this class in the same assembly as the running program, I can't see it or browse for it. What's going on here?
I've had the same problem and it's quite annoying. To work around it I do the following.
Create the setting and type it to Object
Open the settings file in notepad
Change the type to the name of the type in the same assembly (fully qualified name)
Reopen the designer.
Make an innocuous change, hit save and the C# file gets regenerated with your type.