Tuesday, March 20, 2007

Adding Web References to your VS project by code

Today one of my clients asked me to find a way to add web references to existing VS project without using Visual Studio wizard, to do it by code. I built VS add-ins before, so I know what Studio can do, but the question is if I can replace existing system dialogs, without writing a lot of code (I'm really lazy, if I'll write a lot, I'll have no time to write here). So I start to check the material. Really, there is very few information regarding build VS addins, much less regarding creation of references, and nothing about creation web references. So let's deep into VS to understand what's happens there.

While adding web references (the references to Web Services), Visual Studio performs a couple of action. First it checks and retrieves the WS information, then it checks if the current project has local reference to System.Web.Services (the default WinForms project has not). The it discovered the service, by using discovery client protocol, collects the information regarding it's methods by using their public descriptors, then reveals XML schema and based on this schema generates web proxy by using WSDL. Wow, a lot of stuff. How to get rid on it?

Let's discover the world of DiscoveryClientProtocol from Discovery namespace of Web.Services assembly. It has a couple of really interesting methods such as Discover... or Resolve... By using those methods we can create the web service protocol

DiscoveryClientProtocol protocol = new DiscoveryClientProtocol();
            protocol.DiscoverAny(url);
            protocol.ResolveOneLevel();

The next step is to get descriptors. In order to do it, we'll iterate References property of the protocol and create ContractReference and DiscoveryReference for each of methods. We'll add them to our services collection



ServiceDescriptionCollection services = new ServiceDescriptionCollection();
            foreach (DictionaryEntry entry in protocol.References)
            {
                ContractReference contractRef = entry.Value as ContractReference;
                DiscoveryDocumentReference discoveryRef = entry.Value as DiscoveryDocumentReference;
                if (contractRef != null)
                {
                    services.Add(contractRef.Contract);
                }
            }


Let's get the schema of our service in order to generate proxy later it's pretty easy, especially when we have our protocol ready



XmlSchemas schemas = new XmlSchemas();
            foreach (DictionaryEntry entry in protocol.References)
            {
                SchemaReference schemaRef = entry.Value as SchemaReference;
                if (schemaRef != null)
                {
                    schemas.Add(schemaRef.Schema);
                }
            }

Next step is proxy (code) generation. In order to use it, we'll have to ask CodeDom and CodeCompile classes to help up. Actually, we can also call WSDL.exe to generate it, but if we already deep in code, let's create it



ServiceDescriptionImporter importer = new ServiceDescriptionImporter();
 
            foreach (ServiceDescription description in serviceDescriptions)
            {
                importer.AddServiceDescription(description, null, null);
            }
 
            foreach (XmlSchema schema in schemas)
            {
                importer.Schemas.Add(schema);
            }
 
            System.CodeDom.CodeNamespace codeNamespace = new System.CodeDom.CodeNamespace(proxyNamespace);
            CodeCompileUnit codeUnit = new CodeCompileUnit();
            codeUnit.Namespaces.Add(codeNamespace);
            ServiceDescriptionImportWarnings warnings = importer.Import(codeNamespace, codeUnit);
 
            CodeDomProvider provider = new Microsoft.CSharp.CSharpCodeProvider();
            using (StreamWriter sw = new StreamWriter(fileName))
            {
                CodeGeneratorOptions options = new CodeGeneratorOptions();
                options.BracingStyle = "C";
                provider.GenerateCodeFromCompileUnit(codeUnit, sw, options);
            }

Now we have generated files (Reference.map with it's cs, Service.disco and Service.wsdl) all we have to do now is to put them together and reference somehow to our project.



string path = Path.GetDirectoryName(project.FullName)+@"\Web References";
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);
 
            Directory.CreateDirectory(path + @"\" + name);
 
            path += @"\" + name;
 
 
            protocol.WriteAll(path, "Reference.map");
 
            ServiceDescriptionCollection services = GetServiceDescriptionCollection(protocol);
            XmlSchemas schemas = GetXmlSchemas(protocol);
            GenerateWebProxy(name, path + @"\Reference.cs", services, schemas);

So far so good. In order to do something with our project we'll build add-in for Visual Studio, that can pass us the current project and work with it. I'll not explain how to build addins here, ask Google Live to help :)


Just the small explanation for how to get current project, while in DTE _applicationObject.ActiveDocument we only have a document). DTE Document has something, called ProjectItem (we'll meet them closer later) inside this Item we have property named ContainingProject, that actually holds the parent project of this item.



internal static Project GetCurrentProject(Document currDoc)
        {
            if (currDoc == null)
                return null;
 
            ProjectItem prjItem = currDoc.ProjectItem;
            if (prjItem == null)
                return null;
 
            Project currPrj = prjItem.ContainingProject;
            if (currPrj == null)
                return null;
 
            return currPrj;
        }

Good, but in order to add reference to the project we need Visual Studio Project in other words VSProject. This special project holt by secret property of the Project named Object and their namespace is VSLangProj, that dug in VSLangProj assembly in Program Files\Microsoft Visual Studio 8\Common7\IDE\PublicAssemblies (are they really so "public" ? :)


Now let's take a look on this project. Oh my g-d! It has method named ADDWEBREFERENCE, that received url string as input. Just throw all you did off and use it in order to add web reference to your project. Smart VSTS will do all the rest for you!



 



internal static ProjectItem AddWebReference(Project project, string url, string name)
        {
            VSProject vsProj = project.Object as VSProject;
            if (vsProj == null)
                return null;
 
            ProjectItem item = vsProj.AddWebReference(url);
            try
            {
                item.Name = name;
            }
            catch (System.Runtime.InteropServices.COMException ex)
            {
                MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
 
                item.Remove();
            }
 
            return item;
 
        }

Have a nice day,  Visual Studio development and documentation team (this methods exists in MSDN, but researchable for some reason :))


Source code for this article

No comments: