我如何优化 Silverlight 异步 Web 服务消耗
通过一些前期的设计思考,我在当前项目中实现了一些显著的代码优化。
引言
当你从事一个项目时,你很少有时间(有时甚至没有意愿)回过头去重构代码,使其变得 a) 性能更好,或 b) 更易于维护。此外,你没有时间对你的更改进行回归测试,所以通常(也是最推荐的)做法是“保持现状”。我很幸运能够进行这类优化,因为我的日程安排相对灵活(我是“先驱者”),并且我正在等待 IT 人员解决一个服务器问题。
Notice
本文展示了我在代码中实现的内容,仅应作为实现类似结果的示例,并且你应该考虑以下几点
- 由于无法创建必要的基础设施(数据库、Web 服务、.etc,等等),无法为给定技术提供示例应用程序,因此没有可下载的示例代码。
- 由于这不会影响 UI 或任何视觉指示代码正在执行某些操作,因此没有屏幕截图。
- 我假设你熟悉代码中使用的所有术语和基础技术。如果你是初学者,请找人(不一定是我)向你解释其中的细节。
- 最后,这段文字太长了,不能算是一个小技巧,所以我写成了一篇文章。
当前状况
我正在开发一个使用 Web 服务的 Silverlight 应用程序,并根据检索到的数据创建一个多面板图表页面,每个图表需要执行至少两次,(到目前为止)最多四次查询。Silverlight 中 Web 服务的异步性质迫使你处理每次访问 Web 服务的“已完成”事件。在我们的特定案例中,这意味着我们至少需要为一些图表提供五个方法,以便我们能够按正确的顺序级联查询。本质上,它看起来是这样的,并且是每个图表类所必需的
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
}
当然,代码会更庞大一些,但我只包含了处理四次查询图表所需的主要步骤。我觉得我不仅可以抽象出 Web 服务访问代码,还可以减少执行最繁琐查询需求所需的代码量,以下是我的做法。
抽象 - 优化装罐
首先,我利用了 Web 服务本身的架构。对于图表,我们有一个单一的方法,能够调用任何存储过程,并接受你可能想到的任意数量和类型的参数,因为该 Web 服务方法接受要执行的存储过程的名称,以及一个表示存储过程所需参数的 XML 字符串(你可以在我的另一篇技巧中找到详细信息(传递动态参数列表到 Web 服务[^])。Web 服务这一方面是此技术可行的主要原因。
首先,我创建了一个类来包含我的查询参数(存储过程名称和 xml 参数字符串)。如你所见,它只是我们提供给 Web 服务所需内容的容器。
public class QueryItem
{
public string StoredProc { get; set; }
public string XmlParams { get; set; }
public QueryItem(string storedProc, string xmlParams)
{
StoredProc = storedProc;
XmlParams = xmlParams;
}
}
然后,我创建了一个基类,用于派生我的图表对象。以下代码片段中的注释说明了各种数据成员的用途
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()
{
}
我需要一种方法来实际启动检索过程。以下方法初始化控件变量,为 Web 服务方法添加已完成事件处理程序,然后触发对该方法的第一次调用。
//--------------------------------------------------------------------------------
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);
}
接下来,我需要在已完成事件处理程序中添加代码,以检查错误,存储最后一个查询的结果,并在必要时自动启动下一个查询,或发布检索完成事件(以合适的为准)。
//--------------------------------------------------------------------------------
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());
}
}
由于派生图表对象的代码很可能由我以外的人编写,他们可能对如何指示存储过程中的错误有不同的想法。我的做法是返回一个以“Exception”开头的字符串。这意味着空字符串或以“<
”字符开头的字符串都是有效字符串,这就是默认的错误检测行为,但程序员可以自由地想出自己的方法。
//--------------------------------------------------------------------------------
protected virtual bool DataIsOK(string queryResult)
{
bool isOK = (queryResult.StartsWith("<") || queryResult == "");
m_errorEncounter = !isOK;
return isOK;
}
最后,有一个简单的方法可以帮助程序员创建他的查询项。
//--------------------------------------------------------------------------------
protected QueryItem MakeQueryItem(string procName, string xmlData)
{
QueryItem item = new QueryItem(procName, xmlData);
return item;
}
}
用法
在实现了新的基类后,我能够删除超过 400 行不必要的代码。这是在派生类中实现的(通用)代码。在第一个方法中,我们创建查询项,添加已完成事件的处理程序,并调用 DataRetrieval.Retrieve()
方法
//--------------------------------------------------------------------------------
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);
}
最后,我们有了已完成事件处理程序,一旦数据检索完毕,它就会执行所有必要的操作。
//--------------------------------------------------------------------------------
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());
}
结论
请记住,这是我对代码进行的优化,并且仅因为所使用的 Web 服务的设计而可行。请参阅本文前面引用的技巧。
历史
- 2010 年 5 月 26 日 - 原始版本