I'm beginner with C# and XAML.
In my app I read lines of text to list like this:
string path = "ms-appx:///" + _index + ".txt";
StorageFile sampleFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(path));
_stopsList = await FileIO.ReadLinesAsync(sampleFile, Windows.Storage.Streams.UnicodeEncoding.Utf8);
And I put this to combobox2:
comboBox2.ItemsSource = routesList[comboBox.SelectedIndex]._stopsList;
One time I run my app in debug mode, combobox2 is correctly filled with the lines from file (like this 1, but for example next time when I run my app, combobox2 is empty (2) and next to _stopsList appears Count: 0. Content in combobox2 doesn't appear every time.
BusRoute class:
class BusRoute
{
public BusRoute(string name, int index)
{
Name = name;
_index = index;
GetStopsList();
}
public async void GetStopsList()
{
string path = "ms-appx:///" + _index + ".txt";
StorageFile sampleFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(path));
_stopsList = await FileIO.ReadLinesAsync(sampleFile, Windows.Storage.Streams.UnicodeEncoding.Utf8);
}
public string Name
{
get { return _routeName; }
set
{
if (value != null)
{
_routeName = value;
}
}
}
public IList<string> _stopsList = new List<string>();
private string _routeName;
private int _index;
}
MainPage:
public sealed partial class MainPage : Page
{
public MainPage()
{
this.DataContext = this;
this.InitializeComponent();
routesList.Add(new BusRoute("Laszki – Wysocko - Jarosław", 1));
routesList.Add(new BusRoute("Tuchla - Bobrówka - Jarosław", 2));
this.comboBox.ItemsSource = routesList;
this.comboBox.DisplayMemberPath = "Name";
this.comboBox.SelectedIndex = 0;
this.comboBox2.ItemsSource = routesList[comboBox.SelectedIndex]._stopsList;
}
List<BusRoute> routesList = new List<BusRoute>();
}
So the problem here is that GetStopsList() is marked to run async. When you call GetStopsList in the BusRoute constructor, the code continues immediately, and eventually reaches this.comboBox2.ItemsSource = routesList[comboBox.SelectedIndex]._stopsList; At that point the ReadLinesAsync hasn't completed yet (execution in the constructor wasn't paused), so an empty list of data is bound to the comboBox2.
The reason this works when you are debugging is that when you add a break point and inspect the code you are causing an artificial delay which allows enough time for ReadLinesAsync to complete.
Try changing public async void GetStopsList() to public async Task GetStopsList() this will allow the caller to await the function. You then need to call await GetStopsList(); before binding the data list.
You can't await inside the constructor, so you will need to call the intialisation function from somewhere else. This posses an interesting challenge as all your code is inside a constructor. Perhaps there is a Page event you can do this in, e.g. on Load or on Init.
Related
I'm wondering whether there is a clean way to create a new file using Octokit in case the file specified doesn't exist, or otherwise create it? I'm currently attempting to do this via a try-catch block. However, there needs to be a more straightforward way to do this except a try-catch block?
private IRepositoryContentsClient Content => _client.Repository.Content;
public void TransmitLog(string logFilePath)
{
var logContent = LogFileToString(logFilePath);
var githubPath = GenerateGithubPath(logFilePath);
try
{
UpdateLog(githubPath, logContent);
}
catch (NotFoundException)
{
CreateLog(githubPath, logContent);
}
catch (AggregateException)
{
CreateLog(githubPath, logContent);
}
}
private void UpdateLog(string githubPath, string logContent)
{
// MY APP FAILS HERE
var existingFile = Content.GetAllContentsByRef(
_owner, _repo, githubPath, _branch).Result;
// update the file
var relevantSha = existingFile.First().Sha;
var updateRequest = new UpdateFileRequest("Log update" + DateTime.UtcNow, logContent, relevantSha, _branch);
var updateChangeSet = Content.UpdateFile(_owner, _repo, githubPath, updateRequest);
}
private void CreateLog(string githubPath, string logFileContent)
{
// if file is not found, create it
var createRequest =
new CreateFileRequest("Log Creation" + DateTime.UtcNow, logFileContent, _branch);
var createChangeSet = Content.CreateFile(_owner, _repo, githubPath, createRequest);
}
EDIT: I initially had both CreateLog and UpdateLog declared as async void, which led to a whole set of other problems. I edited that out since what I'm really interested in is how to avoid this try/catch structure.
This may not be 100% an EPPlus issue, but since it is Blazor WASM it appears I cannot get the file path to a static image in the wwwroot/images folder. I can get the url and paste it into a browser and that works, even adding that same path to the src attribute of an img works, neither of those helps me.
FYI "background" in this context means a watermark.
It appears that the EPPlus dev team only wants a drive path the file (ex. C:\SomeFolder\SomeFile.png), and I am not seeing how to get that within Blazor WASM. I can get the bytes of the file in c# and even a stream, but no direct path.
My code is the following:
using (var package = new ExcelPackage(fileName))
{
var sheet = package.Workbook.Worksheets.Add(exportModel.OSCode);
sheet.BackgroundImage.SetFromFile("https://localhost:44303/images/Draft.png");
...
}
This returns an exception:
Unhandled exception rendering component: Can't find file /https:/localhost:44303/images/Draft.png
Noticing that leading / I even tried:
sheet.BackgroundImage.SetFromFile("images/Draft.png");
Which returned the same error:
Unhandled exception rendering component: Can't find file /images/Draft.png
So, I am perhaps needing 1 of 2 possible answers:
A way to get a local drive path to the file so the .SetFromFile method is not going to error.
To have a way to set that BackgroundImage property with a byte array or stream of the image. There is this property BackgroundImage.Image but it is readonly.
Thanks to a slap in the face from #Panagiotis-Kanavos I wound up taking the processing out of the client and moving it to the server. With that, I was able to use Static Files to add the watermark with relatively little pain.
In case anyone may need the full solution (which I always find helpful) here it is:
Here is the code within the button click on the Blazor component or page:
private async Task GenerateFile(bool isFinal)
{
...
var fileStream = await excelExportService.ProgramMap(exportModel);
var fileName = "SomeFileName.xlsx";
using var streamRef = new DotNetStreamReference(stream: fileStream);
await jsRuntime.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
}
That calls a client-side service that really just passes control over to the server:
public class ExcelExportService : IExcelExportService
{
private const string baseUri = "api/excel-export";
private readonly IHttpService httpService;
public ExcelExportService(IHttpService httpService)
{
this.httpService = httpService;
}
public async Task<Stream> ProgramMap(ProgramMapExportModel exportModel)
{
return await httpService.PostAsJsonForStreamAsync<ProgramMapExportModel>($"{baseUri}/program-map", exportModel);
}
}
Here is the server-side controller that catches the call from the client:
[Route("api/excel-export")]
[ApiController]
public class ExcelExportController : ControllerBase
{
private readonly ExcelExportService excelExportService;
public ExcelExportController(ExcelExportService excelExportService)
{
this.excelExportService = excelExportService;
}
[HttpPost]
[Route("program-map")]
public async Task<Stream> ProgramMap([FromBody] ProgramMapExportModel exportModel)
{
return await excelExportService.ProgramMap(exportModel);
}
}
And that in-turn calls the server-side service where the magic happens:
public async Task<Stream> ProgramMap(ProgramMapExportModel exportModel)
{
var result = new MemoryStream();
ExcelPackage.LicenseContext = LicenseContext.Commercial;
var fileName = #$"Gets Overwritten";
using (var package = new ExcelPackage(fileName))
{
var sheet = package.Workbook.Worksheets.Add(exportModel.OSCode);
if (!exportModel.IsFinal)
{
var pathToDraftImage = #$"{Directory.GetCurrentDirectory()}\StaticFiles\Images\Draft.png";
sheet.BackgroundImage.SetFromFile(pathToDraftImage);
}
...
sheet.Cells.AutoFitColumns();
package.SaveAs(result);
}
result.Position = 0; // Without this, data does not get written
return result;
}
For some reason, this next method was not needed when doing this on the client-side but now that it is back here, I had to add a method that returned a stream specifically and used the ReadAsStreamAsync instead of ReadAsJsonAsync:
public async Task<Stream> PostAsJsonForStreamAsync<TValue>(string requestUri, TValue value, CancellationToken cancellationToken = default)
{
Stream result = default;
var responseMessage = await httpClient.PostAsJsonAsync(requestUri, value, cancellationToken);
try
{
result = await responseMessage.Content.ReadAsStreamAsync(cancellationToken: cancellationToken);
}
catch (HttpRequestException e)
{
...
}
return result;
}
Lastly, in order for it to give the end-user a download link, this was used (taken from the Microsoft Docs):
window.downloadFileFromStream = async (fileName, contentStreamReference) => {
const arrayBuffer = await contentStreamReference.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
const anchorElement = document.createElement("a");
anchorElement.href = url;
anchorElement.download = fileName ?? "";
anchorElement.click();
anchorElement.remove();
URL.revokeObjectURL(url);
}
The basic idea is I have a UWP app pulling user data from a json file saved locally, and at various times it may pull that full list of objects from the file, but it always checks that the user has set the location for the data, and if not, prompts via a FolderPicker for the user to set the location. In this case, I have combobox that helps filter the objects after selecting a criteria and entering text.
Here's the call stack:
UWPMiniatures.exe!UWPMiniatures.Data.MiniDAL.SetSaveFolder() Line 98 C# Symbols loaded.
UWPMiniatures.exe!UWPMiniatures.Data.MiniDAL.LoadAllAsync() Line 71 C# Symbols loaded.
UWPMiniatures.exe!UWPMiniatures.Data.MiniDataService.Load() Line 36 C# Symbols loaded.
UWPMiniatures.exe!UWPMiniatures.MainPage.FilterGridView(string submission) Line 156 C# Symbols loaded.
UWPMiniatures.exe!UWPMiniatures.MainPage.SearchIcon_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e) Line 95 C# Symbols loaded.
So, working backwords, the FolderPicker is being called here:
private async Task SetSaveFolder()
{
if(!StorageApplicationPermissions.FutureAccessList.ContainsItem("PickedFolderToken"))
{
FolderPicker folderPicker = new FolderPicker();
folderPicker.SuggestedStartLocation = PickerLocationId.Desktop;
folderPicker.FileTypeFilter.Add("*");
folderPicker.CommitButtonText = "Pick A Folder To Save Your Data";
StorageFolder folder = await folderPicker.PickSingleFolderAsync();
if (folder != null)
{
// Application now has read/write access to all contents in the picked folder (including other sub-folder contents)
StorageApplicationPermissions.FutureAccessList.AddOrReplace("PickedFolderToken", folder);
var userFolder = await StorageApplicationPermissions.FutureAccessList.GetFolderAsync("PickedFolderToken");
var file = await userFolder.CreateFileAsync("AllMinisList.json",CreationCollisionOption.OpenIfExists);
var imagefolder = await userFolder.CreateFolderAsync("Images");
}
}
}
The folder picker dialog opens, with a blinking cursor next to Folder:, but nothing happens when I click anywhere, nor can i type in Folder: textbox. Now, putting this identical code in a new project and calling it in response to a click event works fine: Dialog opens, I make a new folder or pick an existing one, it gets added to future access list. Not sure how to else to troubleshoot this but the problem seems to lie out side the actual code calling the FolderPicker.
here is the code for the other calling function
private void SearchIcon_Click(object sender, RoutedEventArgs e)
{
FilterGridView(SearchTextBox.Text);
SearchTextBox.Text = "";
}
private async void FilterGridView(string submission)
{
var selected = FilterComboBox.SelectedValue;
miniDS = new MiniDataService();
if(selected.ToString()=="All")
{
MiniList.Clear();
List<Miniature> fullList = await miniDS.Load();
fullList.ForEach(m => MiniList.Add(m));
}
else if (selected.ToString() == "Quantity")
{
List<Miniature> fullList = await miniDS.Load();
var templist = fullList.AsQueryable()
.Where($"{selected} = #0", submission); ;
MiniList.Clear();
templist.ToList<Miniature>()
.ForEach(m => MiniList.Add(m));
}
else
{
List<Miniature> fullList = await miniDS.Load();
var templist = fullList.AsQueryable()
.Where($"{selected}.StartsWith(#0)", submission);
MiniList.Clear();
templist.ToList<Miniature>()
.ForEach(m => MiniList.Add(m));
}
}
MiniDataService and MiniDal don't do much here other than pass the call along.
Any ideas where I can look to troubleshoot this?
UPDATE: Some additional info, I copied the code from SetSaveFolder() directly into a new event handler for a button, clicked it, I get FolderPicker, functions perfectly. But thats not at all the functionality needed. I need it to be called, directly or indirectly from my Data Service. So here's where its created:
public sealed partial class MainPage : Page
{
/// <summary>
/// MiniList is the list of minis currently being displayed
/// </summary>
private ObservableCollection<Miniature> MiniList;
private MiniDataService miniDS;
private List<string> FilterComboList;
private Miniature NewMini;
public MainPage()
{
this.InitializeComponent();
miniDS = new MiniDataService();
MiniList = new ObservableCollection<Miniature>();
FilterComboList = PopulateFilterCombo();
NewMini = new Miniature();
MyFrame.Navigate(typeof(MiniListPage), MiniList);
}
...
So the problem seems to have something to do with the fact that FolderPicker is being called from this "static" object. Is this a thread issue? I thought in UWP I am always on the UI threadm and since at the top level an event handler is calling folderPicker I can't understand why the UI seems locked.
So i think I figured it out, though I have no idea why this happened. If anyone can clue me in, id appreciate it.
So from the call List<Miniature> fullList = await miniDS.Load();
Here's that method:
public async Task<List<Miniature>> Load()
{
return await minidal.LoadAllAsync();
}
public async Task<List<Miniature>> LoadAllAsync()
{
List<Miniature> MiniCollection = new List<Miniature>();
if (StorageApplicationPermissions.FutureAccessList.ContainsItem("PickedFolderToken"))
{
try
{
var userFolder = await StorageApplicationPermissions.FutureAccessList.GetFolderAsync("PickedFolderToken");
var file = await userFolder.GetFileAsync("AllMinisList.json");
var data = await file.OpenReadAsync();
using (StreamReader stream = new StreamReader(data.AsStream()))
{
string text = stream.ReadToEnd();
MiniCollection = JsonConvert.DeserializeObject<List<Miniature>>(text);
}
}
catch(Exception e)
{
throw e;
}
}
else
{
SetSaveFolder().Wait();
return MiniCollection;
}
return MiniCollection;
}
So the problem was right here:
SetSaveFolder().Wait();
When I replace that with
await SetSaveFolder();
it works fine. I can click in the folderPicker, and it does what it's supposed to. I guess I though .Wait() was used when you aren't return anything other than but it seems there is more too it than that!
I am using GeckoFx to perform a login to a specific website. This website edits the page with new information should the login fail (or require additional authentication, such as a ReCaptcha). Unfortunately, it is vital that I have access an event when the page is updated. I have tried numerous approaches mainly
A continual check if the uri is still the same upon each login attempt and a subsequent check on the specific element in question (to see if the display: none property was changed. (This resulted in an infinite loop as it seems GeckoFx updates the page in a nonblocking way, causing the program to go into an infinite loop)
Sleeping for ~5 seconds between login requests and using the aforementioned uri check. All this did (predictably, I was grasping at straws) was freeze the browser for 5 seconds and still fail to update the page
Searching the GeckoFx codebase for a specific event when the page is updated similar to the DocumentCompleted event (no such luck).
The most common approach I have read about (and one that makes the most sense) is to use a MutationObserver. It seems that all of the answers across the internet involve injecting Javascript in order to perform the requisite task. Seeing as all of my programming background has not touched web development whatsoever, I'm trying to stick to what I know.
Here is my approach so far, unfortunately, it is not much.
public class GeckoTestWebLogin
{
private readonly string _user;
private readonly string _pass;
public GeckoWebBrowser Gweb;
public Uri LoginUri { get; } = new Uri("https://website.com/login/");
public bool LoginCompleted { get; private set; } = false;
public bool Loaded { get; private set; } = false;
public GeckoTestWebLogin(string user, string pass)
{
_user = user;
_pass = pass;
Xpcom.EnableProfileMonitoring = false;
Xpcom.Initialize("Firefox");
//this code is for testing purposes, it will be removed upon project completion
CookieManager.RemoveAll();
Gweb = new GeckoWebBrowser();
Gweb.DocumentCompleted += DocLoaded;
//right about here is where I get lost, where can I set a callback method for the observer to report back to? Is this even how it works?
MutationObserver mutationObserver = new MutationObserver(Gweb.Window.DomWindow, (nsISupports)Gweb.Document.DomObject);
}
private void TestObservedEvent(string parms, object[] objs)
{
MessageBox.Show("The page was changed # " + DateTime.Now);
}
public void DocLoaded(object obj, GeckoDocumentCompletedEventArgs e)
{
Loaded = true;
if (Gweb.Url != LoginUri) return;
AttemptLogin();
}
private void AttemptLogin()
{
GeckoElementCollection elements = Gweb.Document.GetElementsByTagName("input");
foreach (GeckoHtmlElement element in elements)
{
switch (element.Id)
{
case "username":
element.SetAttribute("value", _user);
break;
case "password":
element.SetAttribute("value", _pass);
break;
case "importantchangedinfo":
GeckoHtmlElement authcodeModal =
(GeckoHtmlElement)
Gweb.Document.GetElementsByClassName("login_modal").First();
if (authcodeModal.Attributes["style"].NodeValue != "display: none")
{
InputForm form = new InputForm { InputDescription = "Captcha Required!" };
form.ShowDialog();
elements.FirstOrDefault(x => x.Id == "captchabox")?.SetAttribute("value", form.Input);
}
break;
}
}
elements.FirstOrDefault(x => x.Id == "Login")?.Click();
}
public void Login()
{
//this will cause the DocLoaded event to fire after completion
Gweb.Navigate(LoginUri.ToString());
}
}
As stated in the above code in the comments, I am completely lost at
MutationObserver mutationObserver = new MutationObserver(Gweb.Window.DomWindow, (nsISupports)Gweb.Document.DomObject);
I can't seem to find anything in GeckoFx's source for MutationObserver that would allow me to set a callback/event/whathaveyou. Is my approach the correct way to go about things or am I left with no options other than to inject Javascript into the page?
Much appreciated, thank you in advance.
Here is my attempt at option 2 in Tom's answer:
(Added into GeckoTestWebLogin)
public void DocLoaded(object obj, GeckoDocumentCompletedEventArgs e)
{
Loaded = true;
if (Gweb.Url != LoginUri) return;
MutationEventListener mutationListener = new MutationEventListener();
mutationListener.OnDomMutation += TestObservedEvent;
nsIDOMEventTarget target = Xpcom.QueryInterface<nsIDOMEventTarget>(/*Lost here*/);
using (nsAString modified = new nsAString("DOMSubtreeModified"))
target.AddEventListener(modified, mutationListener, true, false, 0);
AttemptLogin();
}
MutationEventListener.cs:
public delegate void OnDomMutation(/*DomMutationArgs args*/);
public class MutationEventListener : nsIDOMEventListener
{
public event OnDomMutation OnDomMutation;
public void HandleEvent(nsIDOMEvent domEvent)
{
OnDomMutation?.Invoke(/*new DomMutationArgs(domEvent, this)*/);
}
}
I don't think Geckofx's webidl compiler is currently advanced enough to generate the callback constructor.
Option 1. - Enhance MutationObserver source.
You could modify MutationObserver source manually to add the necessary constructor callback. Then recompile Geckofx. (I haven't look to see how difficult this is)
Option 2. - Use old style Mutation events.
public class DOMSubtreeModifiedEventListener : nsIDOMEventListener
{
... // Implement HandleEvent
}
Then something like (maybe in DocumentCompleted event handler):
_domSubtreeModifiedEventListener = new DOMSubtreeModifiedEventListener(this);
var target = Xpcom.QueryInterface<nsIDOMEventTarget>(body);
using (nsAString subtreeModified = new nsAString("DOMSubtreeModified"))
target.AddEventListener(subtreeModified, _domSubtreeModifiedEventListener, true, false, 0);
Option 3. - Use Idle + Check.
Add an winforms Application.idle event handler - and examine the document, to know when its ready.
Option 4. - Inject a javascript callback.
(As you have already mentioned) - This example is waiting until after a resize is done.
basically inject: "<body onresize=fireResizedEventAfterDelay()>" : then inject something like this:
string fireResizedEventAfterDelayScript = "<script>\n" +
"var resizeListner;" +
"var msDelay = 20;" +
"function fireResizedEventAfterDelay() {" +
"clearTimeout(resizeListner);" +
"resizeListner = setTimeout(function() { document.dispatchEvent (new MessageEvent('resized')); }, msDelay);" +
"}\n" +
"</script>\n";
Then in the C#:
browser.AddMessageEventListener("resized", (s) => runafterImDone())
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.