Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

How I Optimized my Silverlight Asynchronous Web Service Consumption

4.95/5 (16 votes)
27 May 2010CPOL4 min read 44.4K  
With some earlier design forethought, I was able to inject some significant code optimization into a current project.

Introduction

When you're working on a project, you rarely have the time (and sometimes even the desire) to go back and re-factor code to make it either a) perform better, or b) easier to maintain. Further, you don't have the time to regression test your changes, so the common (and most recommended) practice is to "leave well enough alone". I am fortunate to be in a position to perform such optimizations because my schedule is somewhat flexible (I'm the "pioneer") and I'm waiting for the IT folks to resolve a server issue.

Notice

This article shows how I implemented something in my code, and should only be used as an example of how to achieve similar results in your own application, and you should consider the following points:

  • Because it's impossible to create the infrastructure necessary (database, web service, .etc, blah, blah) to provide a sample application for the given technique, there is no sample code to download.
  • Because this has no impact on the UI or any visual indication that the code is doing something, there are no screen shots.
  • I am assuming that you are familiar with all of the terms and base-level techniques used in this code. If you're a beginner, get someone (not necessarily me) to explain the finer points to you.
  • Finally, this text is honestly too long to be considered a tip/trick, so I created an article.

The Current Situation

I'm working on a Silverlight application that consumes a web service, and creates a multi-panel chart page based on the data retrieved each chart requires a minimum of two and (so far) a maximum of four queries to be performed. The asynchronous nature of web services in Silverlight forces you to handle "completed" events for each access to the web service. In our particular case, that meant we needed at least five methods for some of the charts so that we could cascade the queries in the correct order. Essentially it looked something like this, and was required for every chart class:

C#
private void RetrieveData()
{
    // add event handler for web service method step1_completed event
    // build parameter string (if necessary)
    // call web service method
}

private void step1_Completed(sender, args)
{
    // process returned dataset
    // handle error condition if necessary
    // remove step1_completed event handler
    // add event handler for web service method step2_completed event
    // build parameter string (if necessary)
    // call web service method
}

private void step2_Completed(sender, args)
{
    // process returned dataset
    // handle error condition if necessary
    // remove step2_completed event handler
    // add event handler for web service method step3_completed event
    // build parameter string (if necessary)
    // call web service method
}

private void step3_Completed(sender, args)
{
    // process returned dataset
    // handle error condition if necessary
    // remove step3_completed event handler
    // add event handler for web service method step4_completed event
    // build parameter string (if necessary)
    // call web service method
}

private void step4_Completed(sender, args)
{
    // process returned dataset
    // handle error condition if necessary
    // remove step4_completed event handler
    // build chart with retrieved data
}

Of course, the code was a bit bulkier, but I only included the major steps required to process a four-query chart. I felt that I could not only abstract out the web service access code, but I could also reduce the amount of code necessary to perform even the most burdensome query requirements, and this is how I did it.

Abstraction - Optimization in a Can

First, I took advantage of the architecture of the web service itself. For the charts, we have a single method capable of calling any stored procedure with any number and type of parameters that you might come up with because that web service method accepts the name of the stored procedure to execute, and a XML string that represents the parameters required by the stored procedure (you can get the low-down on this in one of my other tips (Pass Dynamic List of Parameters to Web Service[^]). This aspect of the web service is primary reason why this technique is viable.

First, I created a class that would contain my query parameters (the stored procedure name and the xml parameter string). As you can see, it's merely a container for the stuff we need to provide to the web service.

C#
public class QueryItem
{
    public string StoredProc { get; set; }
    public string XmlParams { get; set; }
    public QueryItem(string storedProc, string xmlParams)
    {
        StoredProc = storedProc;
        XmlParams  = xmlParams;
    }
}

I then created a base class from which to derive my chart objects. The comments in the following code snippet indicate what the various data members are used for:

C#
public class DataRetrieval
{
    // holds the results of each query (returned by the web service as XML data strings)
    protected List<string>    m_queryResults     = new List<string>();

    // holds the list of query items we're going to hit the web service with (can be 1 or more)
    protected List<QueryItem> m_queryItems       = new List<QueryItem>();

    // tells us when we can stop processing queries
    private   int             m_retrievalCount   = 0;

    // allows the programmer to specify whether to stop querying if an error 
    // condition was triggered in the query
    protected bool            m_stopOnError      = false;

    // inidcates that an error was triggered/detected
    protected bool            m_errorEncountered = false;

    // event handler delegate - for when our query set has been completed

    public delegate void RetrievalCompleteHandler (object sender, MetricEventArgs ca); 

    // event declaration 

    public event RetrievalCompleteHandler RetrievalComplete = delegate{}; 

    //--------------------------------------------------------------------------------
    public DataRetrieval()
    {
    }

I needed a way to actually kick off the retrieval process. The following method initializes the control variables, added the completed event handler for the web service method, and then fired off the first invocation of the method.

C#
//--------------------------------------------------------------------------------
public void Retrieve(bool stopOnError)
{
    m_retrievalCount   = 0;
    m_stopOnError      = stopOnError;
    m_errorEncountered = false;

    if (m_queryItems.Count == 0)
    {
        throw new Exception("No query items specified. Data retrieval process terminated.");
    }

    Globals.service.GetMetricDataCompleted += new EventHandler<SvcReference.GetMetricDataCompletedEventArgs>
                                                              (service_GetMetricDataCompleted);

    Globals.service.GetMetricDataAsync(m_queryItems[m_retrievalCount].StoredProc,
                                       m_queryItems[m_retrievalCount].XmlParams);

}

Next, I needed to add code to the completed event hander that would check for errors, store the results of the last query, and automatically start the next query if necessary, or post the retrieval complete event (whichever was appropriate).

C#
//--------------------------------------------------------------------------------
private void service_GetMetricDataCompleted(object sender, SvcReference.GetMetricDataCompletedEventArgs e)
{
    bool ok = DataIsOK(e.Result);
    bool stop = (this.m_stopOnError && !ok);
    m_queryResults.Add(e.Result);

    if (m_retrievalCount < m_queryItems.Count - 1 && !stop)
    {
        m_retrievalCount++;
        Globals.service.GetMetricDataAsync(m_queryItems[m_retrievalCount].StoredProc,
                                           m_queryItems[m_retrievalCount].XmlParams);
    }
    else
    {
        Globals.service.GetMetricDataCompleted -= new EventHandler<SvcReference.GetMetricDataCompletedEventArgs>
                                                                  (service_GetMetricDataCompleted);
        RetrievalComplete(this, new MetricEventArgs());
    }
}

Because the code for the derived chart objects will probably be written by someone other than myself, they may have different ideas about how to indicate an error in the stored procedure. My way is to return a string that starts with the word "Exception". This means that an empty string, or one that starts with a "<" character is a valid string, so that's the default error detection behavior, but the programmer is free to come up with his own.

C#
//--------------------------------------------------------------------------------
protected virtual bool DataIsOK(string queryResult)
{
    bool isOK = (queryResult.StartsWith("<") || queryResult == "");
m_errorEncounter = !isOK;
    return isOK;
}

Finally, there's a simple method that helps the programmer create his query items.

C#
    //--------------------------------------------------------------------------------
    protected QueryItem MakeQueryItem(string procName, string xmlData)
    {
        QueryItem item = new QueryItem(procName, xmlData);
        return item;
    }
}

Usage

Having implemented the new base class, I was able to remove over a net 400 lines of unnecessary code. Here's the (generalized) code as implemented in the derived class. In the first method, we create our query items, add a handler for the completed event, and call the DataRetrieval.Retrieve() method:

C#
//--------------------------------------------------------------------------------
public void StartRetrieval()
{
    //
    this.m_queryItems.Add(MakeQueryItem(storedProcName1, 
                                        new XElement("Parameters", 
                                                     Globals.MakeXmlParameter("@param1", 0),
                                                     Globals.MakeXmlParameter("@bparam2", "param")).ToString()));
    this.m_queryItems.Add(MakeQueryItem(storedProcName2, 
                                        new XElement("Parameters", 
                                                     Globals.MakeXmlParameter("@param1", 1),
                                                     Globals.MakeXmlParameter("@param2", "param")).ToString()));
    this.m_queryItems.Add(MakeQueryItem(storedProcName3, 
                                        new XElement("Parameters", 
                                                     Globals.MakeXmlParameter("@param1", 2),
                                                     Globals.MakeXmlParameter("@param2", "param2"),
                                                     Globals.MakeXmlParameter("@param3", "param3")).ToString()));
    this.m_queryItems.Add(MakeQueryItem(storedProcName4, 
                                        new XElement("Parameters", 
                                        Globals.MakeXmlParameter("@param1", 3),
                                        Globals.MakeXmlParameter("@param2", "test"),
                                        Globals.MakeXmlParameter("@param3", "test2")).ToString()));
    this.RetrievalComplete += new RetrievalCompleteHandler(dataRetrieval_RetrievalComplete);
    Retrieve(true);
}

Finally, we have our completed event handler which does whatever needs to be done once the data has been retrieved.

C#
//--------------------------------------------------------------------------------
void DelinquentBaseData_RetrievalComplete(object sender, MetricEventArgs ca)
{
    ProcessMainData    (this.m_queryResults[0]);
    ProcessBarchartData(this.m_queryResults[1]);
    ProcessGeneralData (this.m_queryResults[2]);
    this.dataMember   = this.m_queryResults[3];

    // Since this is in a data retrieval class, we have to signal that we have 
    // retrieved and processed our data, and the parent chart object can now populate 
    // the chart panels.
    RaiseDataRetrievalCompletedEvent(new MetricEventArgs());
}

Conclusion

Remember, this is an optimization I performed on my code and was viable only because of the design of the web service being consumed. Please refer to the Tip/Trick cited earlier in this article.

History

  • 26 May 2010 - Original version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)