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:
private void RetrieveData()
{
}
private void step1_Completed(sender, args)
{
}
private void step2_Completed(sender, args)
{
}
private void step3_Completed(sender, args)
{
}
private void step4_Completed(sender, args)
{
}
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.
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:
public class DataRetrieval
{
protected List<string> m_queryResults = new List<string>();
protected List<QueryItem> m_queryItems = new List<QueryItem>();
private int m_retrievalCount = 0;
protected bool m_stopOnError = false;
protected bool m_errorEncountered = false;
public delegate void RetrievalCompleteHandler (object sender, MetricEventArgs ca);
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.
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).
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.
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.
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:
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.
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];
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