I am trying to add a Negative keyword via the CampaignCriterionService, of a Keyword that was retrieved from the SearchTerms Report for a specific Campaign.
I have working code that adds Negative keywords to a Campaign, great. The problem comes from where the Keyword text contains a "special character" (see here)
error:
Keyword text has invalid characters or symbols.. (Error: CriterionError.KEYWORD_HAS_INVALID_CHARS, FieldPath: operations[0].operand.criterion.text, Trigger: plain mirror 100 * 60)
I pulled the keyword from the Search Term report it has a Keyword Id. Therefore I tried to add the keyword as a negative, using the KeywordId using the below
example:
using Google.Api.Ads.AdWords.Lib;
using Google.Api.Ads.AdWords.Util.Reports.v201802;
using Google.Api.Ads.AdWords.v201802;
[TestMethod]
public void AddSingleNegativeKeywordToCampaign()
{
GoogleAdwords.Model.Campaign campaign = new GoogleAdwords.Model.Campaign(CampaignID);
List<Keyword> keywords = new List<Keyword> { new Keyword { id = 27736878000, matchType = KeywordMatchType.EXACT } };
campaign.AddNegativeKeywordstoCampaign(keywords);
}
public class Campaign : AdwordsBase
{
public long CampaignId { get; set; }
public string CampaignName;
public Campaign(long campaignId) : base()
{
CampaignId = campaignId;
}
public void AddNegativeKeywordstoCampaign(List<Keyword> ListofKeywords)
{
AddKeywordstoCampaign(ListofKeywords, Operator.ADD);
}
private void AddKeywordstoCampaign(List<Keyword> keywords, Operator #operator)
{
using (CampaignCriterionService campaignCriterionService =
(CampaignCriterionService)user.GetService(
AdWordsService.v201802.CampaignCriterionService))
{
List<CampaignCriterionOperation> operations = new List<CampaignCriterionOperation>();
foreach (Keyword keyword in keywords)
{
// Create the biddable ad group criterion.
NegativeCampaignCriterion keywordCriterion = new NegativeCampaignCriterion();
keywordCriterion.campaignId = CampaignId;
keywordCriterion.criterion = keyword;
// Create the operations.
CampaignCriterionOperation operation = new CampaignCriterionOperation();
operation.#operator = #operator;
operation.operand = keywordCriterion;
operations.Add(operation);
}
try
{
// Create the keywords.
CampaignCriterionReturnValue retVal = campaignCriterionService.mutate(
operations.ToArray());
// Display the results.
if (retVal != null && retVal.value != null)
{
foreach (CampaignCriterion campaignCriterion in retVal.value)
{
// If you are adding multiple type of criteria, then you may need to
// check for
//
// if (adGroupCriterion is Keyword) { ... }
//
// to identify the criterion type.
Debug.WriteLine("Keyword with Campaign id = '{0}', keyword id = '{1}', text = " +
"'{2}' and match type = '{3}' was {4}.", campaignCriterion.campaignId,
campaignCriterion.criterion.id, (campaignCriterion.criterion as Keyword).text,
(campaignCriterion.criterion as Keyword).matchType, #operator.ToString());
}
}
else
{
Debug.WriteLine("No keywords were added.");
}
}
catch (Exception e)
{
throw new System.ApplicationException("Failed to create keywords.", e);
}
}
}
}
BUT this doesn't work as the api reports the error:
Missing required field.. (Error: RequiredError.REQUIRED, FieldPath: operations[0].operand.criterion.text, Trigger: )
My question is
I can add the search term as a Negative in the Adwords UI, so, therefore, it must be possible. has anybody been able to achieve this using the most recent version of the Adwords API? (v201802)
to be perfectly clear, i don't think there is anything wrong with the code as such, its more of a question about how to add keywords that fall into this remit.
It appears that, true to the documentation here special characters cannot be added to a campaign though any medium.
Related
I'm comparing two graphs, one from a Turtle file with simple literal objects, the other from a file with explicit datatype IRIs. The graphs are otherwise equal.
Graph A:
<s> <p> "o"
Graph B:
<s> <p> "o"^^xsd:string
According to RDF 1.1 (3.3 Literals), "[s]imple literals are syntactic sugar for abstract syntax literals with the datatype IRI http://www.w3.org/2001/XMLSchema#string". This is reflected in the concrete syntax specifications as well (N-Triples, Turtle, RDF XML).
So I'd expect both my graphs to consists of a single triple with a URI node s subject, a URI node p predicate, and a literal node o with type xsd:string object. Based on this I'd expect there to be no difference between the two.
However this is not the case in practice:
var graphStringA = "<http://example.com/subject> <http://example.com/predicate> \"object\".";
var graphStringB = "<http://example.com/subject> <http://example.com/predicate> \"object\"^^<http://www.w3.org/2001/XMLSchema#string>.";
var graphA = new Graph();
var graphB = new Graph();
StringParser.Parse(graphA, graphStringA);
StringParser.Parse(graphB, graphStringB);
var diff = graphA.Difference(graphB);
There's one added and one removed triple in the difference report. The graphs are different, because the datatypes for the object nodes are different: graphA.Triples.First().Object.Datatype is nothing, while graphB.Triples.First().Object.Datatype is the correct URI.
It appears to me that to modify this behaviour I'd have to either
go all the way down to LiteralNode (and change its assumptions about literal nodes), or
create a new GraphDiff (that takes the default datatype of string literals into account).
A workaround is to remove the "default" datatypes:
private static void RemoveDefaultDatatype(IGraph g)
{
var triplesWithDefaultDatatype =
from triple in g.Triples
where triple.Object is ILiteralNode
let literal = triple.Object as ILiteralNode
where literal.DataType != null
where literal.DataType.AbsoluteUri == "http://www.w3.org/2001/XMLSchema#string" || literal.DataType.AbsoluteUri == "http://www.w3.org/2001/XMLSchema#langString"
select triple;
var triplesWithNoDatatype =
from triple in triplesWithDefaultDatatype
let literal = triple.Object as ILiteralNode
select new Triple(
triple.Subject,
triple.Predicate,
g.CreateLiteralNode(
literal.Value,
literal.Language));
g.Assert(triplesWithNoDatatype.ToArray());
g.Retract(triplesWithDefaultDatatype);
}
Is there a way in dotnetrdf to compare simple literals to typed literals in a way that's consistent with RDF 1.1, without resorting to major rewrite or workaround as above?
dotNetRDF is not RDF 1.1 compliant nor do we claim to be. There is a branch which is rewritten to be compliant but it is not remotely production ready.
Assuming that you control the parsing process you can customise the handling of incoming data using the RDF Handlers API. You can then strip the implicit xsd:string type off literals as they come into the system by overriding the HandleTriple(Triple t) method as desired.
Using the Handlers API as per RobV's answer:
class StripStringHandler : BaseRdfHandler, IWrappingRdfHandler
{
protected override bool HandleTripleInternal(Triple t)
{
if (t.Object is ILiteralNode)
{
var literal = t.Object as ILiteralNode;
if (literal.DataType != null && (literal.DataType.AbsoluteUri == "http://www.w3.org/2001/XMLSchema#string" || literal.DataType.AbsoluteUri == "http://www.w3.org/2001/XMLSchema#langString"))
{
var simpleLiteral = this.CreateLiteralNode(literal.Value, literal.Language);
t = new Triple(t.Subject, t.Predicate, simpleLiteral);
}
}
return this.handler.HandleTriple(t);
}
private IRdfHandler handler;
public StripStringHandler(IRdfHandler handler) : base(handler)
{
this.handler = handler;
}
public IEnumerable<IRdfHandler> InnerHandlers
{
get
{
return this.handler.AsEnumerable();
}
}
protected override void StartRdfInternal()
{
this.handler.StartRdf();
}
protected override void EndRdfInternal(bool ok)
{
this.handler.EndRdf(ok);
}
protected override bool HandleBaseUriInternal(Uri baseUri)
{
return this.handler.HandleBaseUri(baseUri);
}
protected override bool HandleNamespaceInternal(string prefix, Uri namespaceUri)
{
return this.handler.HandleNamespace(prefix, namespaceUri);
}
public override bool AcceptsAll
{
get
{
return this.handler.AcceptsAll;
}
}
}
Usage:
class Program
{
static void Main()
{
var graphA = Load("<http://example.com/subject> <http://example.com/predicate> \"object\".");
var graphB = Load("<http://example.com/subject> <http://example.com/predicate> \"object\"^^<http://www.w3.org/2001/XMLSchema#string>.");
var diff = graphA.Difference(graphB);
Debug.Assert(diff.AreEqual);
}
private static IGraph Load(string source)
{
var result = new Graph();
var graphHandler = new GraphHandler(result);
var strippingHandler = new StripStringHandler(graphHandler);
var parser = new TurtleParser();
using (var reader = new StringReader(source))
{
parser.Load(strippingHandler, reader);
}
return result;
}
}
I've been uniting testing two simple examples of compiling CSharpValue activities. One works and the other doesn't I can't figure out why. If someone could point out the issue and optionally a change to correct it if possible.
Details:
The first unit test works SequenceActivityCompile() the second CodeActivityCompile fails with a NotSupportedException (Expression Activity type CSharpValue requires compilation in order to run. Please ensure that the workflow has been compiled.)
I heard somewhere this can be related to ForImplementation but CodeActivityCompile has the same error whether its value is true or false.
This example is a basic adaption of the Microsoft example at: https://msdn.microsoft.com/en-us/library/jj591618(v=vs.110).aspx
This example blog post discussing compiling C# expressions in WF 4+ at length. If anyone reaching this question needs a basic introduction to the topic:
http://blogs.msdn.com/b/tilovell/archive/2012/05/25/wf4-5-using-csharpvalue-lt-t-gt-and-csharpreference-lt-t-gt-in-net-4-5-compiling-expressions-and-changes-in-visual-studio-generated-xaml.aspx
Related Code:
[TestMethod]
public void SequenceActivityCompile()
{
Activity sequence = new Sequence
{
Activities = { new CSharpValue<string>("\"Hello World \"") }
};
CompileExpressions(sequence);
var result = WorkflowInvoker.Invoke(sequence);
}
[TestMethod]
public void CodeActivityCompile()
{
var code = new CSharpValue<String>("\"Hello World\"");
CompileExpressions(code);
var result = WorkflowInvoker.Invoke(code);
}
void CompileExpressions(Activity activity)
{
// activityName is the Namespace.Type of the activity that contains the
// C# expressions.
string activityName = activity.GetType().ToString();
// Split activityName into Namespace and Type.Append _CompiledExpressionRoot to the type name
// to represent the new type that represents the compiled expressions.
// Take everything after the last . for the type name.
//string activityType = activityName.Split('.').Last() + "_CompiledExpressionRoot";
string activityType = "TestType";
// Take everything before the last . for the namespace.
//string activityNamespace = string.Join(".", activityName.Split('.').Reverse().Skip(1).Reverse());
string activityNamespace = "TestSpace";
// Create a TextExpressionCompilerSettings.
TextExpressionCompilerSettings settings = new TextExpressionCompilerSettings
{
Activity = activity,
Language = "C#",
ActivityName = activityType,
ActivityNamespace = activityNamespace,
RootNamespace = null,
GenerateAsPartialClass = false,
AlwaysGenerateSource = true,
ForImplementation = false
};
// Compile the C# expression.
TextExpressionCompilerResults results =
new TextExpressionCompiler(settings).Compile();
// Any compilation errors are contained in the CompilerMessages.
if (results.HasErrors)
{
throw new Exception("Compilation failed.");
}
// Create an instance of the new compiled expression type.
ICompiledExpressionRoot compiledExpressionRoot =
Activator.CreateInstance(results.ResultType,
new object[] { activity }) as ICompiledExpressionRoot;
// Attach it to the activity.
System.Activities.Expressions.CompiledExpressionInvoker.SetCompiledExpressionRoot(
activity, compiledExpressionRoot);
}
I have the following code
class Card
{
string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
class Program
{
static void Main(string[] args)
{
int KaLP = 8000;
int YuLP = 8000;
Card bewd = new Card();
Card LOD = new Card();
var KaibaDeck = new List<Card>() { bewd, LOD };
var KaibaHand = new List<Card>() { };
var KaibaFusionDeck = new List<Card>() { };
var KaibaGraveyard = new List<Card>() { };
var YugiDeck = new List<Card>() { };
var YugiHand = new List<Card>() { };
var YugiFusionDeck = new List<Card>() { };
var YugiGraveyard = new List<Card>() { };
int KaibaDeckSize = KaibaDeck.Count;
string sDrawChoice;
int DrawChoice;
while (KaibaDeckSize > 0)
{
if (DrawChoice == 0)
{
break;
}
else
{
KaibaBattlePhase(KaibaHand, KaibaDeck, KaibaFusionDeck, YugiDeck, Field, KaibaGraveyard, YugiGraveyard, KaLP, YuLP, "Battle");
}
}
}
public static void KaibaBattlePhase(List<Card> KaibaHand, List<Card> KaibaDeck, List<Card> KaibaFusionDeck, List<Card> YugiDeck, List< List<Card> > Field, List<Card> KaibaGraveyard, List<Card> YugiGraveyard, int KaLP, int YuLP, const string Phase)
{
string sDecision;
int Decision;
while(true)//so the while loop NEVER breaks unless you have a 'break' later on
{
Console.WriteLine("Would you like to continue the Battle Phase? (Yes-'1' No-'0', Check Kaiba's Graveyard '-1', Check YUGI's graveyard '-2')");
sDecision = Console.ReadLine();
int.TryParse(sDecision, out Decision);
if (Decision==0)
{
break;
}
if (Decision==-1)
{
KaibaGraveyardFunction(KaibaGraveyard);
}
}
}
public static void KaibaGraveyardFunction(List<Card> KaibaGraveyard)
{
Console.WriteLine("Kaiba's graveyard contains: ");
foreach (Card KG in KaibaGraveyard)
{
Console.WriteLine(KG.Name);
}
}
}
I'm getting the following errors:
On the line KaibaGraveyardFunction(KaibaGraveyard);:
Method must have a return type
At while(true):
Invalid token 'while' in class, struct, or interface member declaration
In addition, I'm getting alot of other errors such as
"Invalid token '(', '=', '.' in class, struct, or interface member declaration"
"Syntax error, ']' expected"
At the end of your method declaration for KaibaBattlePhase you have this method parameter:
const string Phase
That's a syntax error which is confusing the compiler and causing it to not understand pretty much anything afterward. (I've never seen it get that confused from a keyword, usually that happens with missing/extra curly braces and such. But I guess this triggers it too.)
Why are you trying to pass a constant as a method parameter? That doesn't really make a lot of sense. If the method thinks it's a constant, it won't accept any value for it. If you pass it a constant, the method wouldn't know or care where the value came from.
If you remove const then the rest of the errors go away.
(Side note: The error becomes a lot easier to see if you clean up that method declaration. 10 parameters is a lot. It's highly likely that you can get away with some refactoring there to make it a lot cleaner. At its simplest, you can replace all of those parameters with a parameter DTO object which has those as properties. Potentially more useful would be the Replace Method With Method Object pattern, illustrated here.)
Not completely following your logic and not sure this is your issue but you never reevaluate KaibaDeckSize. If your intention is to simply use the while (KaibaDeckSize > 0) {} evaluation as a condition of entering the block, consider replacing it with a simple if statement: if (KaibaDeckSize > 0) {} Also, you never initialize DrawChoice which will generate a Use of unassigned local variable error during compilation. Use: int DrawChoice = 0; And this DrawCount` is never reassigned either.
Did you actually post your most recent code?
int KaibaDeckSize = KaibaDeck.Count;
string sDrawChoice;
int DrawChoice = 0;
while (KaibaDeckSize > 0)
{
if (DrawChoice == 0)
{
break;
}
else
{
KaibaBattlePhase(KaibaHand, KaibaDeck,
KaibaFusionDeck, YugiDeck, Field,
KaibaGraveyard, YugiGraveyard, KaLP, YuLP, "Battle");
}
KaibaDeckSize = KaibaDeck.Count;
}
I need some help "fine-tuning" a block of code in my program which populates an array and binds that array to a dropdown list. Here is the code:
using ([SQL Data Connection])
{
var stakes = from st in ddl.STK_Stakes
where st.STK_EVT_FK == eventId
select new
{
st.STK_Description
};
string[] stakeDesc = new string[stakes.Count()];
foreach (var stake in stakes)
{
stakeDesc[stakeCount] = stake.STK_Description;
stakeCount++;
}
foundDDL.DataSource = stakeDesc;
foundDDL.DataBind();
This code populates Drop-Down List "foundDDL" with options only when foundDDL is on the screen. This code works, but as it's currently used, it is executing every time an instance of foundDDL is created on the screen.
Since the options populated in foundDDL will always be the same while on that page, I want to move this code to its own method, which I can then run once at load time, populate my array, and then just feed that pre-populated array to foundDDL as needed. This will reduce the number of calls to my database and make my program that much more efficient.
The issue I'm having is that I can't figure out how to instantiate my array outside the method, since the number of spaces I'll need in the array is subject to change.
Try using a static property?
private static string[] stakeDesc;
public static string[] StakeDesc {
get {
if(stakeDesc == null)
//initialize it
return stakeDesc;
}
}
You don't have to specify how big it is when you declare it, just declare it outside the method and instantiate it as you are already doing, and if it's already instantiated, just return it.
//In global scope somewhere
string[] stakeDesc;
if (stakeDesc==null || OtherReasonToRefreshData())
{
using ([SQL Data Connection])
{
var stakes = from st in ddl.STK_Stakes
where st.STK_EVT_FK == eventId
select new
{
st.STK_Description
};
stakeDesc = new string[stakes.Count()];
foreach (var stake in stakes)
{
stakeDesc[stakeCount] = stake.STK_Description;
stakeCount++;
}
}
}
foundDDL.DataSource = stakeDesc;
foundDDL.DataBind();
Note you can instantiate the array with 10 elements, than reassign it to a new array with 50 elements without issue.
Because the information does not change, you should be caching this information instead of hitting the DB.
Roughly the code should go (have not compiled this):
class MyCodeBehindClass
{
private List<string> _StakeDesc;//use a dynamically sized array
private List<string> StakeDesc
{
get
{
if (Session[StakeDescKey] != null)
_StakeDesc = (List<string>)Session[StakeDescKey];
else
{
_StakeDesc = GetStakeDesc();
}
return _StakeDesc;
}
set { _StakeDesc = value; }
}
private List<string> GetStakeDesc()
{
using ([SQL Data Connection])
{
var stakes = from st in ddl.STK_Stakes
where st.STK_EVT_FK == eventId
select new
{
st.STK_Description
};
Session[StakeDescKey] = stakes.ToList();//Populate cache
}
}
private void PageLoad()
{
//if not postback...
foundDDL.DataSource = StakeDesc;//Smart property that checks cache
foundDDL.DataBind();
}
}
So I have a Win Phone app that is finding a list of taxi companies and pulling their name and address from Bing successfully and populating a listbox that is being displayed to users. Now what I want to do is, to search for each of these terms on Bing, find the number of hits each search term returns and rank them accordingly (a loose sort of popularity ranking)
void findBestResult(object sender, DownloadStringCompletedEventArgs e)
{
string s = e.Result;
XmlReader reader = XmlReader.Create(new MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(s)));
String name = "";
String rName = "";
String phone = "";
List<TaxiCompany> taxiCoList = new List<TaxiCompany>();
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if (reader.Name.Equals("pho:Title"))
{
name = reader.ReadInnerXml();
rName = name.Replace("&","&");
}
if (reader.Name.Equals("pho:PhoneNumber"))
{
phone = reader.ReadInnerXml();
}
if (phone != "")
{
string baseURL = "http://api.search.live.net/xml.aspx?Appid=<MyAppID>&query=%22" + name + "%22&sources=web";
WebClient c = new WebClient();
c.DownloadStringAsync(new Uri(baseURL));
c.DownloadStringCompleted += new DownloadStringCompletedEventHandler(findTotalResults);
taxiCoList.Add (new TaxiCompany(rName, phone, gResults));
}
phone = "";
gResults ="";
}
TaxiCompanyDisplayList.ItemsSource = taxiCoList;
}
}
So that bit of code finds the taxi company and launches an asynchronous task to find the number of search results ( gResults ) to create each teaxicompany object.
//Parses search XML result to find number of results
void findTotalResults(object sender, DownloadStringCompletedEventArgs e)
{
lock (this)
{
string s = e.Result;
XmlReader reader = XmlReader.Create(new MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(s)));
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if (reader.Name.Equals("web:Total"))
{
gResults = reader.ReadInnerXml();
}
}
}
}
}
The above snipped finds the number of search results on bing, but the problem is since it launches async there is no way to correlate the gResults obtained in the 2nd method with the right company in method 1. Is there any way to either:
1.) Pass the name and phone variables into the 2nd method to create the taxi object there
2.) Pass back the gResults variable and only then create the corresponding taxicompany object?
Right well there is a lot do here.
Getting some small helper code
First off I want to point you to a couple of blog posts called Simple Asynchronous Operation Runner Part 1 and Part 2. I'm not suggesting you actually read them (although you're welcome too but I've been told they're not easy reading). What you actually need is a couple of code blocks from them to put in your application.
First from Part 1 copy the code from the "AsyncOperationService" box, place it in new class file in your project called "AsyncOperationService.cs".
Second you'll need the "DownloadString" function from Part 2. You could put that anywhere but I recommend you create a static public class called "WebClientUtils" and put it in there.
Outline of solution
We're going to create a class (TaxiCompanyFinder) that has a single method which fires off the asynchronous job to get the results you are after and then has an event that is raised when the job is done.
So lets get started. You have a TaxiCompany class, I'll invent my own here so that the example is as complete as possible:-
public class TaxiCompany
{
public string Name { get; set; }
public string Phone { get; set; }
public int Total { get; set; }
}
We also need an EventArgs for the completed event that carries the completed List<TaxiCompany> and also an Error property that will return any exception that may have occured. That looks like this:-
public class FindCompaniesCompletedEventArgs : EventArgs
{
private List<TaxiCompany> _results;
public List<TaxiCompany> Results
{
get
{
if (Error != null)
throw Error;
return _results;
}
}
public Exception Error { get; private set; }
public FindCompaniesCompletedEventArgs(List<TaxiCompany> results)
{
_results = results;
}
public FindCompaniesCompletedEventArgs(Exception error)
{
Error = error;
}
}
Now we can make a start with some bare bones for the TaxiCompanyFinder class:-
public class TaxiCompanyFinder
{
protected void OnFindCompaniesCompleted(FindCompaniesCompletedEventArgs e)
{
Deployment.Current.Dispatcher.BeginInvoke(() => FindCompaniesCompleted(this, e));
}
public event EventHandler<FindCompaniesCompletedEventArgs> FindCompaniesCompleted = delegate {};
public void FindCompaniesAsync()
{
// The real work here
}
}
This is pretty straight forward so far. You'll note the use of BeginInvoke on the dispatcher, since there are going to be a series of async actions involved we want to make sure that when the event is actually raised it runs on the UI thread making it easier to consume this class.
Separating XML parsing
One of the problems your original code has is that it mixes enumerating XML with trying to do other functions as well, its all a bit spagetti. First function that I indentified is the parsing of the XML to get the name and phone number. Add this function to the class:-
IEnumerable<TaxiCompany> CreateCompaniesFromXml(string xml)
{
XmlReader reader = XmlReader.Create(new StringReader(xml));
TaxiCompany result = new TaxiCompany();
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if (reader.Name.Equals("pho:Title"))
{
result.Name = reader.ReadElementContentAsString();
}
if (reader.Name.Equals("pho:PhoneNumber"))
{
result.Phone = reader.ReadElementContentAsString();
}
if (result.Phone != null)
{
yield return result;
result = new TaxiCompany();
}
}
}
}
Note that this function yields a set of TaxiCompany instances from the xml without trying to do anything else. Also the use of ReadElementContentAsString which makes for tidier reading. In addition the consuming of the xml string is much smoother.
For similar reasons add this function to the class:-
private int GetTotalFromXml(string xml)
{
XmlReader reader = XmlReader.Create(new StringReader(xml));
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if (reader.Name.Equals("web:Total"))
{
return reader.ReadElementContentAsInt();
}
}
}
return 0;
}
The core function
Add the following function to the class, this is the function that does all the real async work:-
private IEnumerable<AsyncOperation> FindCompanies(Uri initialUri)
{
var results = new List<TaxiCompany>();
string baseURL = "http://api.search.live.net/xml.aspx?Appid=<MyAppID>&query=%22{0}%22&sources=web";
string xml = null;
yield return WebClientUtils.DownloadString(initialUri, (r) => xml = r);
foreach(var result in CreateCompaniesFromXml(xml))
{
Uri uri = new Uri(String.Format(baseURL, result.Name), UriKind.Absolute);
yield return WebClientUtils.DownloadString(uri, r => result.Total = GetTotalFromXml(r));
results.Add(result);
}
OnFindCompaniesCompleted(new FindCompaniesCompletedEventArgs(results));
}
It actually looks pretty straight forward, almost like synchonous code which is the point. It fetchs the initial xml containing the set you need, creates the set of TaxiCompany objects. It the foreaches through the set adding the Total value of each. Finally the completed event is fired with the full set of companies.
We just need to fill in the FindCompaniesAsync method:-
public void FindCompaniesAsync()
{
Uri initialUri = new Uri("ConstructUriHere", UriKind.Absolute);
FindCompanies(initialUri).Run((e) =>
{
if (e != null)
OnFindCompaniesCompleted(new FindCompaniesCompletedEventArgs(e));
});
}
I don't know what the initial Uri is or whether you need to paramatise in some way but you would just need to tweak this function. The real magic happens in the Run extension method, this jogs through all the async operations, if any return an exception then the completed event fires with Error property set.
Using the class
Now in you can consume this class like this:
var finder = new TaxiCompanyFinder();
finder.FindCompaniesCompleted += (s, args) =>
{
if (args.Error == null)
{
TaxiCompanyDisplayList.ItemsSource = args.Results;
}
else
{
// Do something sensible with args.Error
}
}
finder.FindCompaniesAsync();
You might also consider using
TaxiCompanyDisplayList.ItemsSource = args.Results.OrderByDescending(tc => tc.Total);
if you want to get the company with the highest total at the top of the list.
You can pass any object as "UserState" as part of making your asynchronous call, which will then become available in the async callback. So in your first block of code, change:
c.DownloadStringAsync(new Uri(baseURL));
c.DownloadStringCompleted += new DownloadStringCompletedEventHandler(findTotalResults);
to:
TaxiCompany t = new TaxiCompany(rName, phone);
c.DownloadStringAsync(new Uri(baseURL), t);
c.DownloadStringCompleted += new DownloadStringCompletedEventHandler(findTotalResults);
Which should then allow you to do this:
void findTotalResults(object sender, DownloadStringCompletedEventArgs e)
{
lock (this)
{
TaxiCompany t = e.UserState;
string s = e.Result;
...
}
}
I haven't tested this code per-se, but the general idea of passing objects to async callbacks using the eventarg's UserState should work regardless.
Have a look at the AsyncCompletedEventArgs.UserState definition on MSDN for further information.