2

I am trying to see if I can use EnvDTE from a console program to set the button text on a selected button in the Forms designer in Visual Studio. So far, I have code that will find VS, get a DTE reference, get a Project reference inside the solution, and access a huge number of Project properties in the solution. I can also get a reference to the Forms designer tab/window so that I can print out its caption, for example.

Console.WriteLine ($"Found form window {formwin.Caption}");
Console.WriteLine( $"Formwin count is {formwin.Collection.Count}");

But this is where I get stuck. I have a button selected on a blank form, and would like to find the magic syntax to reference something like formwin.Controls[0].Text = "Hello world."; or something. But I can't figure out how to see the controls (the form and the button) on the surface of the designer and so can't set the text property of the button. And I can't find any documentation on doing this kind of thing, short of a reference to record a macro to get some hints. (There's an extension to download and install and try, but I don't know yet if it will work for stuff like this. Most macro recorders do text only.)

Could anyone shed some light on the syntax or where I might find examples or documentation on how to do this? Thank you.

UPDATE 1: Some code to show some progress because of TnTinMn's help. If I make the invalid cast, I can see all the properties that I expect to see for Forms objects in the text editor. But at runtime, it fails.

The opening line of the loop raises an InvalidCastException because it's invalid to cast a System.__ComObject to a System.ComponentModel.ComponentCollection.

IDesignerHost foo = (IDesignerHost) formwin.Object;
IContainer container = foo.Container;
// almost works but foreach throws InvalidCastException at runtime below
foreach (IComponent component in container.Components) {
    Console.WriteLine (component.GetType ().Name + " : " + component.Site.Name);
}

UPDATE 2: More code to help the helpers... I'll clean it up after an answer is found.

For now, I'm a little bit further than described in the original post above. I have a __ComObject in my hand and can cast it in the text editor and can see all the Controls and Properties that I want to see and use. BUT... at runtime I get InvalidCastExceptions that say I can't cast __ComObjects to Forms or ComponentCollections or whatever.

Here are two example exceptions, followed by my current console app code. My solution has 1 project in it (ConsoleApp1), which contains 1 Forms1.cs file and 1 Program.cs file.


Error Message 1:
 Item caption is Error List ...

Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'System.__ComObject' to type 'System.ComponentModel.ComponentCollection'.
   at System.StubHelpers.InterfaceMarshaler.ConvertToManaged(IntPtr pUnk, IntPtr itfMT, IntPtr classMT, Int32 flags)
   at System.ComponentModel.IContainer.get_Components()
   at ConsoleApp1.Program.Main(String[] args) in E:\Projects\DteProject1Solution\ConsoleApp1\Program.cs:line 116
Press any key to continue . . .


Error Message 2:
  Item caption is Stack Trace Explorer - Stack Trace

Unhandled Exception: System.InvalidCastException: Unable to cast COM object of type 'System.__ComObject' to class type 'System.Windows.Forms.Form'. Instances of types that represent COM components cannot be cast to types that do not represent COM components; however they can be cast to interfaces as long as the underlying COM component supports QueryInterface calls for the IID of the interface.
   at ConsoleApp1.Program.Main(String[] args) in E:\Projects\DteProject1Solution\ConsoleApp1\Program.cs:line 107
Press any key to continue . . .

Here is my current almost-working code, with a trace of its output. I wonder about the formwin variable - I think it's the Form1.cs window, but formwin.Collection.Count is 13, which are all the usual VStudio windows (editor windows, solution explorer, error list, toolbox, and so on). Maybe formwin.Collection points back to the collection that holds it?

Output of my code:

E:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\devenv.exe
E:\Projects\DteProject1Solution\ConsoleApp1\Program.cs
Active doc name is 'Program.cs'
Count of doc.windows is 1
Looping over dte.Solution.Projects:
  Project name is ConsoleApp1
  Project name is Miscellaneous Files
Number of projects in solution: 2
Finding the ConsoleApp1 project:
  Project name is ConsoleApp1
Found project ConsoleApp1
Myproject has a ProjectItem count of 4
  Project Type is System.__ComObject
DTE.Windows.Count is 14
Looping over dte.Windows to find 'Form1.cs':
  Window is Form1.cs [Design]
Found form? dteclientarea? window Form1.cs [Design]
Window count of formwin.Collection.Count is 14
Looping over formwin.Collection: (obvious VStudio windows)
  Item caption is Form1.cs [Design]
  Item caption is Program.cs
  Item caption is Junk.txt
  Item caption is DteProject1
  Item caption is Toolbox
  Item caption is Output
  Item caption is Solution Explorer
  Item caption is Properties
  Item caption is Locals
  Item caption is Call Stack
  Item caption is Diagnostic Tools
  Item caption is Disassembly
  Item caption is Error List ...
  Item caption is Stack Trace Explorer - Stack Trace

Unhandled Exception: System.InvalidCastException: Unable to cast COM object of type 'System.__ComObject' to class type 'System.Windows.Forms.Form'. Instances of types that represent COM components cannot be cast to types that do not represent COM components; however they can be cast to interfaces as long as the underlying COM component supports QueryInterface calls for the IID of the interface.
   at ConsoleApp1.Program.Main(String[] args) in E:\Projects\DteProject1Solution\ConsoleApp1\Program.cs:line 107
Press any key to continue . . .

And here is the code itself.

class Program
{
  static void Main (string[] args) {
    void Dprint (string msg) {
      Console.WriteLine (msg);
    }

    var allDTEs = Ide.GetAllDte ();
    var dte = allDTEs[0];
    Console.WriteLine (dte.FileName);
    Console.WriteLine (dte.ActiveWindow.Document.FullName);
    var doc = dte.ActiveWindow.Document;
    Dprint ($"Active doc name is '{doc.Name}'");
    var n = doc.Windows.Count;
    Dprint ($"Count of doc.windows is {n}");

    Dprint ("Looping over dte.Solution.Projects:");
    Projects projectlist = dte.Solution.Projects;
    foreach (Project proj in projectlist) {
      Dprint ($"  Project name is {proj.Name}");
    }

    Dprint ($"Number of projects in solution: {projectlist.Count}");
    Dprint ("Finding the ConsoleApp1 project:");
    // This fails: projectlist(idx) error: Method name expected.
    //var foundproject = 0;
    //for (int idx = 1; idx <= projectlist.Count; idx++) {
    //  Project proj = projectlist (idx);
    //  if (proj.Name == "ConsoleApp1") {
    //    foundproject = idx;
    //  }
    //}

    Project myproj = null;
    foreach (Project proj in projectlist) {
      Dprint ($"  Project name is {proj.Name}");
      if (proj.Name == "ConsoleApp1") {
        myproj = proj as Project;
        break;
      }
    }

    Dprint ($"Found project {myproj.Name}");
    var n2 = myproj.ProjectItems.Count;
    Dprint ($"Myproject has a ProjectItem count of {n2}");
    foreach (Property prop in myproj.Properties) {
      //Write ($"  Property is {prop.Name}"); // 20 or 30 of them print
      if (prop.Name == "ProjectType") {
        // Myproject type is System.__ComObject
        Dprint ($"  Project Type is {myproj.Properties.Item ("ProjectType")}");
      }
    }

    Dprint ($"DTE.Windows.Count is {dte.Windows.Count}");
    Window formwin = null;
    Dprint ($"Looping over dte.Windows to find 'Form1.cs':");
    foreach (Window win in dte.Windows) {
      Dprint ($"  Window is {win.Caption}");
      if (win.Caption.Contains ("Form1")) {
        formwin = win as Window;
        break;
      }
    }

    if (formwin == null) {
      Dprint ($"Form window is null.");
      return;
    }

    Dprint ($"Found form? dteclientarea? window {formwin.Caption}");
    Dprint ($"Window count of formwin.Collection.Count is {formwin.Collection.Count}");
    Dprint ($"Looping over formwin.Collection: (obvious VStudio windows)");
    foreach (Window item in formwin.Collection) {
      Dprint ($"  Item caption is {item.Caption}");
    }

    // foo becomes a __ComObject, theform becomes null
    IDesignerHost foo = (IDesignerHost) formwin.Object;
    Form theform = (Form) foo.RootComponent;
    if (theform != null) {
      Dprint ($"Code name of theform.Name is {theform.Name}");
    }

    IContainer container = foo.Container;
    foreach (IComponent component in container.Components) {
      Console.WriteLine (component.GetType ().Name + " : " + component.Site.Name);
    }
  }

Thank you again to those who are trying to help.

Kevin
  • 1,548
  • 2
  • 19
  • 34
  • Kevin, sharing a [MCVE] will help users to help you. – Reza Aghaei Dec 08 '19 at 09:20
  • 1
    Are you sure you want to have a console application to manipulate Windows Forms Designer? What's the real requirement? – Reza Aghaei Dec 08 '19 at 09:28
  • @RezaAghaei, the real requirement is to get external programmatic access to the property sheet during the design cycle. A console app is just an easy way of writing code to prove that external (non-vstudio, non-vs-plugin, non-vs-addin, non-vs-extension) access is possible. I can get external access to paragraphs in Word, slides in PowerPoint, and so on. Now I want access to objects in Visual Studio Forms, if possible. I didn't think my broken code would help jump the barrier that TnTinMn identified. I will post some code after I clean it up. – Kevin Dec 08 '19 at 21:54
  • Are you creating a new designer [like this](https://stackoverflow.com/a/57474278/3110834) or are you trying to have some interaction with an open instance of Visual Studio? Could you please explain the scenario a bit more? For example tell us: I want to get an open instance of VS, find Project1 in the Solution1, and if the designer of Form1 is open, change the BackColor property of Button1. – Reza Aghaei Dec 09 '19 at 01:47
  • You may also want to take a look at this post: [Get all controls of the current form at design-time](https://stackoverflow.com/a/54255416/3110834). – Reza Aghaei Dec 09 '19 at 01:56
  • Thanks Kevin for the code. Could you please explain the scenario as well, like what I asked in above comment? I think I still haven't get the scenario (However, I'm still not sure/convinced what you are trying to do is the path to go.) – Reza Aghaei Dec 09 '19 at 05:07
  • @RezaAghaei Hi, the scenario explained is just what I said; there is no work requirement. I want to see if I can programmatically add/delete/modify elements on a form designer sheet from *outside* vstudio. In the bigger picture, I want to see if I can write external driver "macros" for vstudio in a VSTO-like fashion like I can for Excel, Word, and PowerPoint. I found an example of how to insert text in the VS text buffer, but it seemed more challenging and useful to try to work the property sheet (or the object itself) in the designer. I hope that helps. – Kevin Dec 09 '19 at 15:46
  • Which VS Version? – Reza Aghaei Dec 09 '19 at 16:37
  • I'm not sure if it's possible for VS2017 outside of the VS process. – Reza Aghaei Dec 09 '19 at 17:36
  • @RezaAghaei The latest community version, 2019. – Kevin Dec 10 '19 at 01:38
  • The sample task that I'm trying to do is basically automating VS from a console application, open VS → opening a Solution → open a Form in design mode → Select an existing control (a button) → Set back color → Save the Form. I'm not sure if it's easily possible with VS>=2017. – Reza Aghaei Dec 11 '19 at 09:00
  • @RezaAghaei That sample task is bigger than the one I was trying for; I had VS open with label selected. The issue at runtime is casting the __ComObject that comes across the COM interface to a VS object that enables access to the properties and things inside of VS. There is some kind of an Intellisense cast going on in the text editor that lets me see all the properties that I'm after. But at runtime, the code either returns null using the ".. as Form" style of casting or raises an InvalidCastException (using the direct (Form) style of casting). Very strange. – Kevin Dec 11 '19 at 16:53
  • I have no problem at opening VS, Opening Solution, and Opening the specific Form in a specific Project. But the problem is (the same problem you also are encountering), casting the RootComponent of the IDesignerHost. It's probably because of a specific registration model of COM component by VS 2017, I guess. – Reza Aghaei Dec 11 '19 at 17:10
  • Same problem with VS2015. – Reza Aghaei Dec 11 '19 at 19:26
  • I didn't try more to solve the problem, however I'm curious to know if you had any progress to solve the problem? – Reza Aghaei Dec 22 '19 at 16:53
  • Hi Reza, thank you for your help at trying the problem. I'm sorry that I did not make any more progress either. You sounded like an expert with far more skills than I have, so I thought that if you could not do it then there was no hope for me (or probably anyone else). – Kevin Dec 23 '19 at 06:51

0 Answers0