Angel Hernández

WWSAPI (Windows Web Services API) + Interop + WPF… Chévere!!!

Hace un par de noches atrás estaba leyendo sobre las características nuevas del SDK de Windows 7 y puedo decir que  son bastantes, de hecho en una de las sesiones que tuve en Tech-Ed este año mencioné algunas de ellas. Sin embargo, una que llamó mi atención fue WWSAPI pues hasta ahora no existía soporte para código nativo, excepto por un par de Toolkits existentes entre ellos gSOAP que puede utilizarse con Windows, Linux y Mac OSX. WWSAPI fue presentado en el PDC del año pasado (2008) y será liberado formalmente con Windows 7 aunque versiones anteriores del sistema operativo, a partir de XP SP2. Yo lo probé inicialmente cuando Windows 7 estaba en RC sin embargo no había posteado al respecto por esperar que Windows 7 fuese liberado o cerca de serlo.

Una vez dicho esto, durante el fin de semana creé una solución para demostrar WWSAPI en conjunto con .NET. La solución tiene la estructura mostrada a continuación

Solution 

  • NativeTester: Aplicación de Consola (C++) que llama a la biblioteca de enlace dinámico (DLL)
  • RSSFeedService: Servicio Web (C#) que recupera entradas RSS, las parsea y regresa el resultado como XML
  •  RSSViewer: Aplicación basada en WPF (C#) que consume el servicio Web e invoca a la biblioteca de enlace dinámico (DLL)
  • Tester: Aplicación basada en WinForm (C#) que consume el servicio Web e invoca a la biblioteca de enlace dinámico (DLL)
  • WWSAPIDemo: Biblioteca de enlace dinámico (C++) que implementa WWSAPI y es invocada desde .NET a través de Interop

Así mismo con el código pueden encontrar MSDN.xml que son las entradas RSS de la página de MSDN Australia, aunque el código apunta a http://localhost/MSDN.xml, ustedes pueden cambiar dicho Url y apuntar al RSS que gusten, en mi caso guardé el archivo de entradas RSS y lo pusé en my IIS local.

Muchos de ustedes se preguntarán, ¿cómo genero el proxy del servicio Web desde C++? Y la respuesta es muy simple, el nuevo SDK de Windows 7 trae consigo un utilitario que lo hace por nosotros,  WSUTIL.exe. La ejecución nos da como resultado dos archivos (uno .H y otro .CPP). Las cadenas por defecto son tratadas como WCHAR* que es Unicode y todas las cadenas en .NET son interpretadas como Unicode, más adelante comentaré un poco de esto.  

wsutil

Con el servicio Web ya publicado en IIS, entonces nos queda comenzar a trabajar en la biblioteca de enlace dinámico que será cliente e implementará WWSAPI. A continuación el archivo de cabecera de la biblioteca

#include "stdafx.h"
 
#define EXPORT extern "C" __declspec(dllexport)
 
#define MAX_SIZE  128000 //128Kb
#define TRIM_SIZE 512
 
EXPORT WCHAR* GetFeeds(WCHAR* szUrl, int cbResults);
 
EXPORT void GetItem(WCHAR* szUrl, WCHAR* szItemName);
así como el método que se conecta al servicio Web y recupera las entradas de RSS
// Get feeds from a given Url through WWSAPI
EXPORT WCHAR* GetFeeds(WCHAR* szUrl, int cbResults) {
    WCHAR* temp = NULL;
    WS_HEAP* heap = NULL;
    WCHAR retval[MAX_SIZE]; // 128Kb (It should be enough to avoid WS_E_QUOTA_EXCEEDED error)
    WS_ERROR* error = NULL;
    ULONG propertiesCount = 0;
    WS_SERVICE_PROXY* proxy = NULL;
    WS_CHANNEL_PROPERTY channelProps[2]; 
    WS_STRING serviceUrl = WS_STRING_VALUE(L"http://localhost/DemoSvc/RSSFeedService.asmx");
    WS_ENDPOINT_ADDRESS endpoint = { serviceUrl}; 
    WS_ENVELOPE_VERSION soapVersion = WS_ENVELOPE_VERSION_SOAP_1_1; // Our Webservice is WsiProfiles.BasicProfile1_1 compliant
    WS_ADDRESSING_VERSION addressingVersion = WS_ADDRESSING_VERSION_TRANSPORT;
 
    // Set channel's properties
    channelProps[propertiesCount].id = WS_CHANNEL_PROPERTY_ENVELOPE_VERSION;
    channelProps[propertiesCount].value = &soapVersion;
    channelProps[propertiesCount].valueSize = sizeof(soapVersion);
    propertiesCount++;
 
    // Set addressing's properties
    channelProps[propertiesCount].id = WS_CHANNEL_PROPERTY_ADDRESSING_VERSION;
    channelProps[propertiesCount].value = &addressingVersion;
    channelProps[propertiesCount].valueSize = sizeof(addressingVersion);
    propertiesCount++;
 
    // Can we create an WsError and WsHeap objects?
    if (SUCCEEDED(WsCreateError(NULL, NULL, &error)) && SUCCEEDED(WsCreateHeap(MAX_SIZE, TRIM_SIZE, NULL, NULL, &heap, error))) {
        // Can we create a proxy based on the service?
        if  (SUCCEEDED(WsCreateServiceProxy(WS_CHANNEL_TYPE_REQUEST, WS_HTTP_CHANNEL_BINDING, NULL, NULL, NULL, 
            channelProps, propertiesCount, &proxy, error))) {
                // Can we open the proxy object?
                if  (SUCCEEDED(WsOpenServiceProxy(proxy, &endpoint, NULL, error))) {
                    // If we're able to invoke the service then copy the results to another variable because if
                    // we don't we lose the response after freeing the heap
                    if (SUCCEEDED(RSSFeedServiceSoap12_RetrieveFeeds(proxy, szUrl, cbResults, &temp, heap, NULL, NULL, NULL, error))) 
                        wcscpy_s(retval, temp);
                }
        }
    }
 
    // Deallocate and free resources
    if (error != NULL)
        WsFreeError(error);
 
    if (proxy != NULL) {
        WsCloseServiceProxy(proxy, NULL, error);
        WsFreeServiceProxy(proxy);
    }
 
    if (heap != NULL)
        WsFreeHeap(heap);
 
    return retval;
}

Por favor, nótese lo siguiente:

  • Debemos especificar la versión de SOAP (de lo contrario, el cliente se va a quejar al respecto, porque va a utilizar 1.2. Prueba de esto, es el nombre del método RSSFeedServiceSoap12) 
  • Regreso un WCHAR* y funciona sin problemas, aunque la manera correcta debería ser es regresar un HRESULT o un entero, tomar un puntero como parámetro que al mismo tiempo sirve de valor de retorno. Un documento sobre las mejores prácticas de desarrollo de DLLs puede encontrarse aquí

Nuestra implementación desde la aplicación de consola es mostrada a continuación

#include "stdafx.h"
 
typedef WCHAR* (*myCallback) (WCHAR*, int);
 
int _tmain(int argc, _TCHAR* argv[]) {
    HINSTANCE hInstance;
    myCallback ptrToFunc;
    WCHAR* results = NULL;
 
    if ((hInstance = LoadLibrary(L"C:\\Users\\angel.hernandez\\Desktop\\WWSAPI\\WWSAPIDemo\\x64\\Debug\\WWSAPIDemo.dll")) != NULL) {
        if ((ptrToFunc = (myCallback) GetProcAddress(hInstance, "GetFeeds")) != NULL) {
            results = ptrToFunc(L"http://localhost/MSDN.xml", -1);
            wprintf(L"\n%ls\n", results);
            FreeLibrary(hInstance);
            printf("\n\nPress any key to exit...\n");
            _getch();
        }
    }
    return 0;
}

Al compilar y ejecutar nuestra aplicación de prueba NativeTester, podemos ver como se muestran las entradas recuperadas en la consola.

NativeTester

La otra función contenida en la biblioteca de enlace dinámico es GetItem, que muestra el elemento encontrado tras la ejecución de una consulta de XPath y haciendo uso de la función starts-with (como si fuera el operador Like %). La función GetItem recupera las entradas RSS al llamar previamente a la función GetFeeds.

// Execute XPATH Query based on feeds retrieved through WWSAPI
EXPORT void GetItem(WCHAR* szUrl, WCHAR* szItemName) {
    WCHAR retval[TRIM_SIZE];
    WCHAR* results = NULL;
    BSTR nodeContent = NULL;
    WCHAR xPathQueryBuffer[TRIM_SIZE];
    IXMLDOMDocumentPtr docPtr = NULL;
    IXMLDOMNodePtr selectedNode = NULL;
 
    if ((results = GetFeeds(szUrl, -1)) != NULL &&  wcslen(results) > 0) {
        CoInitialize(NULL);
 
        docPtr.CreateInstance("Msxml2.DOMDocument.6.0");
 
        wsprintf(xPathQueryBuffer, L"/rss/items/item[starts-with(@title,'%ls')]", szItemName);
 
        if (SUCCEEDED(docPtr->loadXML(_bstr_t(results), NULL))) {
            if (SUCCEEDED(docPtr->selectSingleNode(_bstr_t(xPathQueryBuffer), &selectedNode))) {
                nodeContent = SysAllocString(retval);
                selectedNode->get_xml(&nodeContent);
                MessageBox(NULL, nodeContent, L"XPath Query Results", NULL);
                SysFreeString(nodeContent);
            }
        }
    }
}

Hasta ahora hemos logrado implementar y utilizar WWSAPI desde código nativo, sin embargo aún no hemos probado la funcionalidad de nuestra biblioteca de enlace dinámico desde .NET, en ese caso lo primero que debemos hacer es importar la función que nos interesa a través de DllImport

[DllImport(@"C:\Users\angel.hernandez\Desktop\WWSAPI\WWSAPIDemo\x64\Debug\WWSAPIDemo.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr GetFeeds(IntPtr szUrl, int cbResults);

La  función GetFeeds retorna un WCHAR* que es traducido a NET como un IntPtr, así mismo hacemos uso  de la clase Marshal para traducir el WCHAR* a una cadena Unicode y para pasar un WCHAR* a la función, como es mostrado a continuación

private void btnTestWWSAPI_Click(object sender, EventArgs e) {
    IntPtr szUrl = Marshal.StringToBSTR("http://localhost/MSDN.xml");
    MessageBox.Show(Marshal.PtrToStringUni(GetFeeds(szUrl, -1)));
    Marshal.FreeBSTR(szUrl);
}

Y Voila!!! Tengo mi implementación de WWSAPI siendo utilizada desde .NET, bueno al menos desde una aplicación Windows Form, pero ¿cómo será con una aplicación WPF? Pues… la respuesta es será igual, a diferencia de que debes escribir el XAML y que hacer binding a los objetos es más sencillo si se utiliza un XmlDataProvider pues los datos se extraen y asignan utilizando una sintaxis basada en XPath.

<!-- Data Provider-->
<Window.Resources>
    <XmlDataProvider x:Key="xmlFeeds" IsAsynchronous="True" XPath="/rss/items/item"/>
</Window.Resources>
 
<Grid Name="grdFeeds" Margin="0,75,0,12">
    <!-- Binding Source-->
    <ListView Margin="11.432,6" Name="lstFeeds"  
             ItemsSource="{Binding Source={StaticResource xmlFeeds} }" MouseDoubleClick="lstFeeds_MouseDoubleClick">
        <ListView.View>
            <GridView x:Name="grdFeedItems">
                <GridViewColumn Header="Title" x:Name="grcTitle" Width="100" DisplayMemberBinding="{Binding XPath=@title}"/>
                <GridViewColumn Header="Publishing Date" x:Name="grcPublishingDate" Width="100" DisplayMemberBinding="{Binding XPath=@publishingDate}"/>
                <GridViewColumn Header="Url" x:Name="grcUrl" Width="100" DisplayMemberBinding="{Binding XPath=@url}" />
            </GridView>
        </ListView.View>
    </ListView>
</Grid>
 

El manejador del evento Click del botón para llamar a nuestra biblioteca de enlace dinámico es mostrado a continuación

private void btnExecuteOperation_Click(object sender, RoutedEventArgs e) {
    XmlDocument feeds = new XmlDocument();
    IntPtr szUrl = IntPtr.Zero, szItemName = IntPtr.Zero;
    XmlDataProvider xmlFeeds = TryFindResource("xmlFeeds") as XmlDataProvider;
 
    try {
        switch (cboOperations.SelectedIndex) {
            case 0:
                using (RSSFeedService proxy = new RSSFeedService())
                    feeds.LoadXml(proxy.RetrieveFeeds("http://localhost/MSDN.xml", -1));
                xmlFeeds.Document = feeds;
                break;
            case 1:
                szUrl = Marshal.StringToBSTR("http://localhost/MSDN.xml");
                feeds.LoadXml(Marshal.PtrToStringUni(GetFeeds(szUrl, -1)));
                Marshal.FreeBSTR(szUrl);
                xmlFeeds.Document = feeds;
                break;
            case 2:
                szUrl = Marshal.StringToBSTR("http://localhost/MSDN.xml");
                szItemName = Marshal.StringToBSTR(!string.IsNullOrEmpty(txtItemTitle.Text) ? txtItemTitle.Text : "Windows");
                GetItem(szUrl, szItemName);
                Marshal.FreeBSTR(szUrl);
                Marshal.FreeBSTR(szItemName);
                break;
        }
    } catch (Exception ex) {
        MessageBox.Show(string.Format("Oops! Something wrong just occurred\n{0}", ex.Message),
            "Exception caught", MessageBoxButton.OK, MessageBoxImage.Information);
    }
}

El post tiene adjunto el código mostrado, pueden descargarlo, modificarlo y jugar con él. Espero que sea de utilidad y recuerden, la única manera de aprender es jugar con la tecnología y no tener miedo para asumir nuevos retos.

Que Dios los bendiga.

Saludos,

Angel

Leave a Comment

(required) 

(required) 

(optional)

(required)