in

SharePoint Blogs

The Best Place for SharePoint-related Blogs

pm4everyone's blog

July 2006 - Posts

  • SharePoint and Groove Web Services Part III -- Adding Tools

    We authenticated in Part I, and created Workspaces in Part II, and we are about to add our first tools here in Part III. But first I'd like to revisit SharePoint and Groove workspace creation, for I did them a disservice by not talking about custom templates. These are templates created by you, that describe either the components of a workspace, or describe a component itself.

    In Part II I stated that Groove's workspace creation is designed to be much more generic than SharePoint's Document Workspace. This is not entirely true. As a few people pointed out, and as Microsoft's Working with Templates page attests, you can in fact create, load and instantiate custom templates with SharePoint. SharePoint calls these Site Templates and List Templates. In Groove nomenclature, these are called Groove Space Archives (.gsa file extension) and Groove Tool Archives (.gta file extension) and while they're not techinically templates, you can use them as such.

    Where SharePoint and Groove differ with regard to populating custom spaces is how they differ with populating custom tools themselves. You cannot register a custom template with SharePoint strictly using WSS. With Groove GWS, however, you can, sort of. SharePoint site templates and Groove space archives are really immaterial to the problem of created templated workspaces. The root of the problem lies with the tool templates. By this I mean both SharePoint and Groove allow you to create empty workspaces and to add new tools based on a template-- so your tool layout could exist purely in your smart client code if you wished.

    For SharePoint, you must be use their HTML based administration UI to add a custom template to the system, which then allows WSS to add that custom template to a workspace. SharePoint then acts as its own distribution mechanism for the template, but cannot propogate the template to other SharePoint servers.

    Groove, on the other hand, because of its peer-to-peer nature, can. In fact, Groove used to allow you to add a general template as long as you identified a server on the internet to fetch it from (this is how our first PM tool works). You can't do that anymore, but you can add a Groove Form with a custom schema, design and view, and not need to identify a server to fetch it from. Why? Because a Groove Form is self-contained. And once its registered on an endpoint, it is propogated to other endpoints by Groove itself. And this process of adding a custom Groove Form can be done by GWS.

    But that's yet another part. Hugh Pyle has been extremely helpful in showing me how this works, but I have to work through it myself first before I can write about it. In the meantime, on to the this weeks installment: Adding tools to SharePoint and Groove.

    What is a workspace without any tools? Not much. So both SharePoint and Groove provide a set of default tools a developer can add via web services. SharePoint and Groove also provide a way to create default workspaces that contain a default set of tools. As we saw in Part II, a SharePoint Document workspace comes packaged with tools like Announcements, Share Documents, Task List and such, while Groove's default workspace contained only Files, Discussion and Members. (By the way, this is an disparity we hope to correct soon!).

    But what if our SharePoint space needs a second Document Library tool, and our Groove workspace needs a Groove Form? We add them with the code outlined below.

    For SharePoint, I need to identify the tool template I would like to use. Assuming I have the workspace in mind, maybe even a workspace I just created, I would use the Webs service method

    GetListTemplates();

    This will return an XML string you can parse for specific template names and identifiers. It's generally human readable, which is good, since you do have to know what you're looking for. But once you've identified the template id for a Document Library ("101"), you could code something like:

    public void AddDocumentLibrary() {
        // We've gotten the templates via Webs, now we use the Lists service
        // to add a tool based on a template
        SharePointProxies.Lists l = new SharePointProxies.Lists();
      
        // We're caching our credentials
        l.Credentials = this.Credentials;
      
        // and using the workspace we created in Part II
        l.Url = "http://myserver/myroot/uniquename" + "/_vti_bin/lists.asmx";
      
        // The result xml from the AddList method XmlNode resultNode = null;
     
       // the display name of the tool we will add
       string toolName = "My Display Name";

       // its template id
        int toolID = 101;

        try {

            // if successful, the result will be XML we can parse for
            resultNode = l.AddList( toolName, "My Description", toolID );

            // the result id, which looks a alot like a GUID
            // I like to check for both name and id
            resultID = parseForID( toolName, toolID.ToString() );
       
        }

       catch ( System.Exception ex ) {
            reportError( aReporter, ex.Message );
        }

       finally {

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

       }

    }

    // a simple routine to find the ID from the AddList result XML
    private string parseForID( System.Xml.XmlNode xmlNode, string name, string templateID ) {

        System.Xml.XmlAttributeCollection attrs = xmlNode.Attributes;
      
        XmlNode idNode = attrs.GetNamedItem( "ID" );
        XmlNode titleNode = attrs.GetNamedItem( "Title" );
        XmlNode serverTemplateNode = attrs.GetNamedItem( "ServerTemplate" );

        string id = ( idNode == null ) ? "" : idNode.Value;
        string title = ( titleNode == null ) ? "" : titleNode.Value;
        string serverTemplate = ( serverTemplateNode == null ) ? "" : serverTemplateNode.Value;

        if ( title == name && templateID == serverTemplate )
            return id;
        else
            return null;

    }

    The end result of the above code is the addition of a new Document Library to the SharePoint workspace. An interesting thing about this particular tool is it also supports Web Distributed Authoring and Versioning, or WevDAV. This is a nice tool in two respects: 1) You have a choice of using SharePoint WSS or WebDAV to work with it and 2) It supports versioning-- which is very hard to do in a purely peer-to-peer environment.

    The pattern for adding a Groove Form to a new Groove workspace is very similar. First we call the Groove version GetListTemplates, which is part of the Tools service and is called:

    ReadAvailableTools();

    Also similar to SharePoint is you have to know what you are looking for. This call returns an array of templates you can iterate through. Each template contains a human readable Name and a not so human readable ComponentResourceURL (your remember those?). The ComponentResourceURL is like the SharePoint template identifier. The code for adding a Groove tool via GWS follows:

    // Here is an OSD URL that identifies a Groove Form and its version
    private static string s_grooveFormComponentResourceURL = "http://components.groove.net/Groove/Components/Root.osd?Package=net.groove.Groove.Tools.Business.GrooveForms.Groo veForms_TPL&Version=5&Factory=Open";

    public string AddGrooveForm() {
        // Tools is the container we add a Tool to
        GrooveProxies.Tools tools = new GrooveProxies.Tools();

        // refer to Part I or Part II for more detail
        buildRequestHeader( tools );

        // refer to Part I or Part II for more detail
        tools.Url = m_grooveHost + m_grooveTelespaceTools;

        // instantiae a tool to fill out
        GrooveProxies.Tool tool = new GrooveProxies.Tool();

        // with name
        tool.Name = "My Display Name";

        // and all-important identifier
        tool.ComponentResourceURL = s_grooveFormComponentResourceURL;

        // the result is the tool identifier you can append to
        // m_grooveHost
        string toolID = tools.Create( tool );

         if ( toolID == null || toolID.Length == 0 )
             return null;
        else
            return toolID;

    }

    private bool acquireTelespaceTools() {

        // We go through Spaces container to find the Space tool
        // to find the Tools container.
        GrooveProxies.Spaces spaces = new GrooveProxies.Spaces();

        // refer to Part I or Part II if you
        // want to see headers built
        buildRequestHeader( spaces );

        // again, refer to Part I or Part II for more detail
        spaces.Url = m_grooveHost + m_grooveTelespace;

        // retrieve data about the space
        GrooveProxies.Space space = spaces.ReadSpace();

        if ( space == null )
            return false;

        // retrieve the Tools container for the space
        m_grooveTelespaceTools = space.Tools;

        return true;

    }

    The Groove Forms tool also has lots of nifty features, but we'll get to them in another installment, I promise. In the meantime, what you have seen is how to add not just SharePoint Document Libraries or Groove Forms, but any tool given the proper template id. For each system, you could iterate through the templates returned by either GetListTemplates or ReadAvailableTools and add them to a workspace, thus creating a workspace containing every possible web part or Groove tool.

    In this case, SharePoint and Groove are nearly identical in how you discover and add tools. The real differences lie in the tools themselves. Each tool tends to, understandably, acquiesce to the strengths and weaknesses of their architecture. The SharePoint Document Library supports versioning as well as robust internet protocols, while the Groove Form supports peer-to-peer distribution of schema, design and content.

    We will take a more detailed look at both in Part IV.

  • SharePoint and Groove web services comparison and how to -- Part II

    We left off with how you get past the authentication gate and talk to both SharePoint and Groove. SharePoint uses the familiar challenge/Response pattern and Groove implements a localhost/registry solution. Now that we have our clearance, let's see what awaits us on the inside. Here is a table outlining the basic services for each system:

    SharePoint

    Administration Manage Site Collection
    Alerts Alerts for list items
    Document Workspace Manage document workspaces
    Forms HTML Display forms for list content
    Imaging Manage picture libraries
    Lists Working with lists and data
    Meetings Manage Meeting workspaces
    Permissions SharePoint Services Security
    Sites Information about workspace templates
    Users and Groups Manage users and groups
    Versions Working with file versions
    Views Presentation views of Lists
    Webs Managing workspaces and subworkspaces



    Groove

    Accounts Provide Account and Identity info
    Calendar Manage Calendar Tool
    Contacts Contact info for Identities
    Discussion A Form presented as a Discussion tool
    Events Read events you subscribe to
    FilesBase64 A Files tool in Base64
    FilesDIME A Files tool in DIME binary attachments
    Forms Highly configurable Groove Form (Kind of like HTML Form, but more powerful)
    Local Methods that invoke the Groove UI
    Members Workspace membership info
    Messages Read and Send instant messages
    Properties Version info for Groove client and web services
    Spaces Manage Groove workspaces
    Subscriptions Monitor Groove events with subscriptions
    Tools Manage tools within a Workspace
    VCard Detailed info for an Identity

    I've been claiming that SharePoint and Groove are more similar than not. What do you suppose a merging of the two might look like? While its not a one-to-one correspondence, there are definitely similarities between the two web service APIs. But first let's define a few terms:

    A Workspace is a SharePoint Site is a Groove Workspace.
    A Tool is a SharePoint Web Part is a Groove Tool.
    A User is a SharePoint User is a Groove Account and Identity.

    And finally, the most important:

    A Form is a SharePoint List is a Groove Form.

    Given the above terms, I can claim that the following web services are similar:

    SharePoint Groove
    Administration, Sites Spaces
    Alerts Events, Subscriptions
    Document Workspace Space*
    Meetings Space*
    Lists, Views, Forms Forms
    Users and Groups  Accounts, Members
    Webs Spaces


    *Groove follows a container/item pattern whereby the web service often exposes a container, which in turn manipulates specific items within that container. For spaces, we can ask Groove for all workspaces, identify a particular Space, and then drill down for more information.

    So let's do some sample code. First up is SharePoint.

    Now that I can talk to SharePoint, I'd like to create a Workspace. First I must determine where I want to locate it. SharePoint follows a hierarchical convention much like a file system. It happens to be a database under the covers, but the effect is that standard URL addressing works just fine.

    Let's say we have a SharePoint server set up at

    http://myserver

    And that we've set up a root site at

    /myroot

    We can then address the location we want to base our creation as

    http://myserver/myroot

    So we use the SharePoint Document Workspace Server (which we will call DWS) in the following code:

    // the Document Workspace Service
    SharePointProxies.Dws dws = new SharePointProxies.Dws();

    // we're caching our credentials
    dws.Credentials = this.Credentials;

    // the web service address
    dws.Url = "http://myserver/myroot" + "/_vti_bin/dws.asmx";

    string result = "";

    try {

        result = dws.CreateDws( "uniquename", "", "My Title -- Hello World!", "" );

    }
    catch ( System.Net.WebException webex ) {
        reportError( webex );
    }
    catch ( System.Exception sysex ) {
        reportError( sysex );
    }
    finally {

        if ( dws != null ) {
            dws.Dispose(); // Don't forget to dispose these web service calls!
        dws = null; // I try to help the garbage collector whenever I can.
    }

    Uri newWorkspaceLocation = parseResultForUri( newWorkspaceLocation );



    And voila, you have created a new Document Workspace 'under' http://myserver/myroot with an address of

    http://myserver/myroot/uniquename

    Now what is a Document Workspace? It is a blank workspace with the following web parts added:

    1) Announcements
    2) Shared Documents
    3) Tasks
    4) Contact List
    5) Event List
    6) Discussion
    7) Surveys
    8) Links
    9) Members

    Obviously all these web parts weren't individually represented in the SharePoint services defined above. What most of them are (Announcements, Tasks, Contacts, Events, Discussion, Links, Surveys and Share Documents) are permutations of a SharePoint List. You can verify this by making a SharePointProxies.Lists.GetListCollection call, which tells you the lists in a SharePoint workspace. In fact, the only thing that is not a list is the Members web part. Hmmm... interesting...

    And now let's look at instantiating a Groove Workspace.

    Groove has a shallow addressing scheme, which means yes there is a hiearchy for workspaces, but its a single parent/child relationship. You can organize workspaces into hiearchical folders via the Groove Launchbar UI, however, for addressing purposes, you usually work with an url like:

    http://localhost:9080/<groove service>/<identifier>

    The Groove version of the SharePoint Document Workspace Service is the Spaces service. Spaces is short for Workspaces and, unlike SharePoint, Groove provides the unique identifier for you. In fact, Groove usually provides the unique identifier for everything.

    Here is code to create a Groove Workspace.

    // This bundle of info is an identifier for the Groove default workspace.
    // It's a more generic version of the SharePoint Document Workspace
    // in that there are many different Component Resource URLs you
    // could use to instantiate a space. This one specifies we
    // should use version 4 of the default workspace, which
    // comes with a files tool and a discussion tool.
    private static string s_grooveWorkspaceComponentResourceURL =
    "http://components.groove.net/Groove/Components/Tools/System/SystemTools.osd?Package=net.groove.Groove.Tools.Syste
    m.GrooveDefault_CST&amp;Version=4&amp;Factory=Open";


    // remember the context that Groove needs?
    // this assumes you went through the discovery process in Part I
    // and have cached the necessary values.
    private void buildRequestHeader( WSDLProxies.Spaces spaces ) {
            
        spaces.GrooveRequestHeaderValue = new GrooveProxies.GrooveRequestHeader();
        spaces.GrooveRequestHeaderValue.GrooveRequestKey = m_grooveRequestKey;
        spaces.GrooveRequestHeaderValue.GrooveIdentityURL = m_grooveIdentity;
            
    }

    private bool createGrooveWorkspace() {

        // Spaces is the container service for Space operations.
        GrooveProxies.Spaces spaces = new GrooveProxies.Spaces();
        buildRequestHeader( spaces );
        spaces.Url = m_grooveHost + "/GWS/Groove/2.0/Spaces/";

        // setdata to the particular item
        GrooveProxies.Space space = new GrooveProxies.Space();
        space.Name = title;

        // use the container to invoke the operation
        string grooveTelespace = spaces.Create( space, s_grooveWorkspaceComponentResourceURL );
     
        // grooveTelespace now contains the <groove service> and <identifier>
        // so you can concatenate m_grooveHost and grooveTelespace for future
        // calls.
    }

    The result of createGrooveWorkspace is a new, default Groove Workspace. Now what is a default Groove Workspace? As of today, it is a blank workspace with the following tools added:

    1) Files
    2) Discussion
    3) Members

    The files web service is provided by either GrooveFilesBase64 or GrooeFilesDIME. The discussion web service is provided by GrooveDiscussion, however, it is actually a GrooveForm with discussion semantics-- much like those SharePoint lists masquerading as various tools. And the member web service is provided by GrooveMembers.

    As we noted above, Groove's workspace creation is designed to be much more generic than SharePoint's Document Workspace. It used to be, in Groove, you could create your own workspace templates and feed that workspace's component resource url and presto, you'd have a workspace designed to your template specs. However, I don't believe this feature is available anymore. Maybe in the future.

    What you can do is manipulate individual tools to configure the Groove workspace how you'd like. For instance, our product will be adding a Groove Forms tool. However, the Groove Forms tool cuts a wide swath of functionality, and really deserves its own part.

    Perhaps Part III?

    Finally, for all you old timers out there, no you weren't dreaming. You were probably thinking that Groove component resource URL looked a lot like something from the Open Software Description Specification. And in fact, you would be correct. Pre-Groove 2007 you could use OSD specifications to inject new functionality into Groove.

    This is on hiatus for the moment, but I wouldn't be a bit surprised if OSD injection becomes available once again in a future version.

  • SharePoint and Groove web services comparison and how to -- Part I

    I've always wanted to start a series. And while I'm no Alexandre Dumas, I'm sure the topic will be no less exciting: Comparing SharePoint web services with Groove web services. OK, so maybe there's not as much intrigue but with the convergence of SharePoint and Groove its no less interesting and quite a bit more relevant.

    My company produces Project Management software for both SharePoint and Groove. We just released our SharePoint product that uses web services and while we've been shipping a Groove tool for the past three years, we're making a transition to web services for Groove 12. Over the course of several web posts, I will share my experiences with both web service platforms and hope to draw a conclusion on where things are headed. As a teaser, I believe SharePoint and Groove will oneday converge since their models are very, very similar. I talk about this a little bit here.

    Note: for brevity, I will refer to SharePoint Web Services as WSS, and Groove Web Services as GWS. Also, I will assume you can set up the web service proxies for both systems. There's plenty of help for WSS. For GWS, a good jumping off point is Hugh Pyle's blog. I'm here to help you get up and running. On to the story....

    Everything has a beginning and when talking web services you have to start with authentication. If you're not authenticated, you won't get far. So how to authenticate?

    For both systems, authentication information is bundled into each web service request. However, how they bundle the information is quite different.  With WSS, each web service request contains credential information. This is standard SOAP that works with the System.Net.CredentialCache. For example:

    SharePointProxies.Lists l = new SharePointProxies.Lists();
    l.Credentials = this.System.Net.CredentialCache.DefaultCredentials.


    If your default credentials are not sufficient, you will need to create your own credentials, preferably with the credentials UI Microsoft provides. This is the standard Challenge/Response scenario common to Client/Server... oops, I mean Smart Client/Web Service computing.

    That's about it for SharePoint since you can only log in as a single entity. Now that you have the proper credentials, you can access WSS for site information, member lists, tools and such. Just be sure to keep that credential handy.

    Groove is a different beast, however. While it is a web service endpoint, it is in fact the world's smartest smart client. By this I mean you will most likely be accessing GWS via localhost on the same computer as your application. Because of this, GWS does not use System.Net.ICredentials, but instead creates a separate authentiaction requirement: windows registry lookups for key values.

    What's happening is this: because Groove is a smart client most likely running on your local machine, it sets keys for access into the CURRENT_USER hive in your registry. Specifically:

    // Read GrooveLocalHTTPPort
    Microsoft.Win32.RegistryKey grooveRegKey =     Microsoft.Win32.Registry.CurrentUser.OpenSubKey( "Software\\Microsoft\\Office\\12.0\\Groove" );
    string grooveHost= "http://localhost:" + grooveRegKey.GetValue(    "GrooveLocalHTTPPort" ).ToString();

    // Read LocalRequestKey
    Microsoft.Win32.RegistryKey grooveWebServicesRegKey  = Microsoft.Win32.Registry.CurrentUser.OpenSubKey( "Software\\Microsoft\\Office\\12.0\\Groove\\WebServices");
    string grooveRequestKey = (string)grooveWebServicesRegKey.GetValue( "LocalRequestKey");


    There is a third key for remote access, but it's left to the user to transport the key value from the computer running GWS to the machine that wants to talk to GWS. Because these keys are generated every time your start Groove, its a non-trivial challenge. The port is also generated by Groove in that 9080 is preferred, but it will bounce higher if another process has already grabbed 9080. So we perform the lookup on this as well.

    Once you have the keys, you still must figure out who you want to be. This is done by accessing the GrooveAccounts. You can have multiple user accounts in Groove, much like multiple logins in Windows. Additionally, each account can have multiple identities. GWS wants to know exactly what Identity you want to be in order to work. Maybe you only have one Account with one Identity, in which case life is simple. But if you have multiple Accounts with multiple Identities, then you will need to settle on one. Here is how you can iterate through the choices:

    // Create the GrooveAccounts service
    GrooveProxies.Accounts accounts = new GrooveProxies.Accounts();
    accounts.GrooveRequestHeaderValue = new GrooveProxies.GrooveRequestHeader();
    accounts.GrooveRequestHeaderValue.GrooveRequestKey = grooveRequestKey;
    accounts.Url = grooveHost + "/GWS/Groove/2.0/Accounts/";
     
    // Read the accounts-- this is the actual Web Service Call
    GrooveProxies.Account2 [] result = accounts.Read2();

    // Display accounts and identities
    foreach ( GrooveProxies.Account2 account in result) {

        System.Console.WriteLine( account.Name );

        foreach ( GrooveProxies.Identity2 identity in account.Identities) {

            System.Console.WriteLine( "\t" + identity.Name );

        }
       
    }


    Once you select your Identity, you have the GWS equivalent of the WSS System.Net.ICredential.

    Did you notice the GrooveRequestHeader? This is how GWS extends basic SOAP packets to include its own keys and identity information. Whereas WSS uses a basic SOAP property

    SharePointProxies.Lists.Credentials

    GWS requires that you use:

    GrooveRequestHeader

    for authentication. And remember, those GWS key values will change every time the Groove process is restarted, so you can't cache keys. Instead, you should have a routine that does lookups for you when you start a series of operations. Also, unlike WSS, you cannot use the Microsoft credentials UI since GWS does not use System.Net.ICredentials. In fact, Groove has its own login UI that will challenge the user for a password. However, people (like me) often choose the 'Remember Password' option in Groove and rely on a successful Windows login for user validation. Once GWS is accessible, it will provide you the choice of Accounts and Identities you can use.

    Wouldn't it be nice if Groove used the System.Net.ICredentials system as well? I suspect this will happen in the future.

    Congratulations, you've just been cleared to talk WSS and GWS! But remember that this is a series. We will find out how to gather information about Sites and Workspaces in Part II.





  • SharePoint Web Part vs. Smart Client using WSS

    Suppose you wanted to build a Project Management solution and you thought SharePoint was pretty neat. Then you got a group of programmers you respected and you knew could do the job. Then imagine this group of programmers had to decide whether to build a web part or a smart application. At the end of the discussion would you a) have consensus and begin coding right away or b) have to come back the next day with doughnuts and alms to ease the tension and hash it out again.

    If you're like us, then the answer was .... errr... (b). The arguments were plentiful and forceful, but mostly boiled down to ease of deployment and management for web parts vs richer UI and offline mobility for smart clients.

    Before I get too far down the road, however, I should point out how overweighted towards web parts the SharePoint community seems to be. For instance, doing a cursory search of SharePointBlogs.com yielded only two(!) mentions of smart client technology:

    Henk Hooiveld's blog on SharePoint and Office
    Todd Baginski's SharePoint 2003 and MOSS 2007 blog

    Obviously the developer community has spoken and it's web parts in a runaway. But the right decision for a particular circumstance is the right decision, even if everyone else thinks you're wrong smile.

    For me it came down to the nature of the application and mobility. Mind you, I have my set of biases. Before SharePoint we developed a product for Groove (also project management), which carries smart client technology to the nth degree (must have been doing something right though, since Microsoft bought them and created the SharePoint, Groove and Project group).

    The application aspect of the decision is the most rock solid for me. Project Management has enough complexity that it can be difficult to manipulate the various components without a lot of serious DHML and vbscript. And even then it seems problematic to do drag and drop scheduling in an interactive gantt chart through your browser without an ActiveX control of some sort-- and that would obviate the east of deployment advantage. While some approximation has been done by a few companies, it has taken them several iterations to approach basic usability. And we wanted to make usability one of our selling points.

    The mobility aspect of our decision really comes down to when you believe will happen. I fully believe that some day no matter where you are, you will be able to access the internet. But that day has not arrived yet, and it won't arrive by the end of this decade either. And the day it does arrive, it actually won't because that will be the day they announce public travel to the moon, with internet connectivity coming soon. But maybe you reading this at Starbucks on your Danger Sidekick and shaking your head at the crumudgeon who just doesn't get it. Well, go start your own company wink.

    Now I absolutely acknowledge the inherent distribution superiorty of a web application, or web part. However, I would argue its not quite as superior as it once was. Sure, if faced with the choice of purchasing boxes of software and installing them machine by machine, then I bow down before the web server and chant 'ease of deployment'. HOWEVER... those were the bad old days. Today, tools like SMS and technology like .NET reduce the web application distribution advantage to almost even. I recall a recent conversation with an IT person at a Fortune 500 company who stated they can update all 15,000 machines with new software in under 60 minutes.

    So our decision came do to this: If you believe your application is complex enough and you don't believe in an 'always on anywhere' internet, then the benefits of a smart client will outweigh the single (but substantial) benefit of deployment. If you want to build the world's best name, rank and serial number data entry application, then I think a web part makes absolute perfect sense. But if you want to create an app that that is focused on user experience and furthers what's possible with todays rich UIs, then a rich, smart client is the way to go.

    We certainly hope so.







Need SharePoint Training? Attend a SharePoint Bootcamp!

Posts (c) their respective authors. Everything else (c) 2007 SharePoint Experts