Monotouch.Dialog StyledStringElement delegate always fires for the last element added - xamarin.ios

given the following code, I am having an issue when clicking on each element. If we assume I have 5 exercises and therefore create 5 elements in the foreach() loop, when the table is rendered and I click on any element, the delegate always gets the exercise of the 5th (last) element.
The elements are displayed properly, each showing the associated exercise's name. It is just the delegate that does not work as expected.
If I do not use a foreach loop and hardcode each element instead it works as expected. However if I cannot dynamically populate the dialogViewController and use the element tapped event for each one, is not good.
private void CreateExerciseTable()
{
Section section = new Section();
foreach (var exercise in exercises)
{
var element = new StyledStringElement(exercise.ExerciseName,
delegate { AddExercise(exercise); })
{
Font = Fonts.H3,
TextColor = UIColor.White,
BackgroundColor = RGBColors.LightBlue,
Accessory = UITableViewCellAccessory.DisclosureIndicator
};
section.Elements.Add(element);
}
var root = new RootElement("Selection") {
section
};
var dv = new DialogViewController(root, true);
dv.Style = UITableViewStyle.Plain;
//Remove the extra blank table lines from the bottom of the table.
UIView footer = new UIView(new System.Drawing.RectangleF(0,0,0,0));
dv.TableView.TableFooterView = footer;
dv.TableView.SeparatorColor = UIColor.White;
dv.TableView.BackgroundColor = UIColor.White;
tableFitnessExercises.AddSubview(dv.View);
}
private void AddExercise(FitnessExercise exercise)
{
NavigationManager.FitnessRoutine.Add(exercise);
PerformSegue(UIIdentifierConstants.SegAddExerciseToFitnessRoutine, this);
}

This is a classic closure bug!
The problem is that you are accessing the loop reference.
Try:
foreach (var exercise in exercises)
{
var localRef = exercise;
var element = new StyledStringElement(exercise.ExerciseName,
delegate { AddExercise(localRef); })
{
Font = Fonts.H3,
TextColor = UIColor.White,
BackgroundColor = RGBColors.LightBlue,
Accessory = UITableViewCellAccessory.DisclosureIndicator
};
section.Elements.Add(element);
}
For more on this see http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx

Related

UINavigationBar in a UIPopoverController looks ugly if created programatically

I want to open a Popover in code after a specific action happens (e.g. tap on a button).
The following Code let me open a Popover with a NavigationBar but it don't looks like the one I've done with IB. (IB approach was embedding a UIViewController in a UINavigationController and defining the the two buttons cancel and save). The only thing I want is having these two buttons on top of the Popover. I don't use any navigation functionality!
var cell = grid.VisibleCellAtCoordinate(coordinate) as SDataGridTextCell;
var viewController = new UIViewController();
var navBar = new UINavigationBar(new RectangleF(0, 0, viewController.View.Bounds.Width, 50))
{
AutoresizingMask = UIViewAutoresizing.FlexibleWidth,
BarStyle = UIBarStyle.Black,
Translucent = true,
Items = new[]
{
new UINavigationItem("test")
{
LeftBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, (s, e) => { _popoverController.Dismiss(true); }),
RightBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Save, (s, e) => { _popoverController.Dismiss(true); })
}
}
};
var textField = new UITextField(new RectangleF(0, 50, viewController.View.Bounds.Width, viewController.View.Bounds.Height)) { Placeholder = "Mein Text...", BackgroundColor = UIColor.White };
textField.BecomeFirstResponder();
viewController.View.Add(navBar);
viewController.View.Add(textField);
viewController.View.SubviewAdded(textField);
_popoverController = new UIPopoverController(viewController);
_popoverController.SetPopoverContentSize(new SizeF(250, 200), false);
_popoverController.PresentFromRect(cell.Bounds, cell, UIPopoverArrowDirection.Any, false);
These buttons looks like this image (ugly):
And this image shows how it's look like when created in IB:
That's one of the caveats of using a standalone navigation bar in a popover. Embedding your view controller in a navigation controller is, as far as I know, the only way to avoid it.
Had some problem embedding the content view (UIViewController) correctly to the UINavigationController because I tried to add the navigation bar items (buttons) each time directly to the UINavigationController's NavigationItem property. But this must be done through the content view controller...
var contentViewController = new UIViewController();
var textField = new UITextField(contentViewController.View.Bounds)
{
BackgroundColor = UIColor.White,
AutoresizingMask = ~UIViewAutoresizing.None,
Placeholder = "My Text..."
};
contentViewController.View.Add(textField);
var navigationController = new UINavigationController(contentViewController);
contentViewController.Title = "Popover";
contentViewController.NavigationItem.LeftBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, (s, e) => { _popoverController.Dismiss(false); });
contentViewController.NavigationItem.RightBarButtonItem = new UIBarButtonItem(UIBarButtonSystemItem.Save, (s, e) => { _popoverController.Dismiss(false); });
_popoverController = new UIPopoverController(navigationController);
_popoverController.SetPopoverContentSize(new SizeF(300, 300), false);
_popoverController.PresentFromRect(button1.Bounds, button1, UIPopoverArrowDirection.Any, false);

MonoTouch Message Element opes page with string elements

I have the following code:
Section _section = new Section ("Test");
foreach (ExampleData data in Example.data) {
MessageElement Item = new MessageElement (){
Sender = data.Name,
Subject = data.Value,
Body = data.Description,
Date = data.Modified
} ;
_section.Add(Item);
var root = new RootElement("Item Expanded"){
new Section ("test2"){
new StringElement("Field Name", data.FieldName),
new StringElement("Value", data.Value),
new StringElement("Description", data.Description)
}
} ;
_section.Add(root);
} ;
var _rootElement = new RootElement ("Items") {
_section
} ;
I would like this to work in such a way that when a Message Element is tapped it shows the section with ("test2") that has the same data (e.g. the data was added during the same run of the loop.) I realize this will not happen currently, as it seems the Message Element
requires an Action delegate to do anything on a tap event, plus I'm adding everything to the same section. However, is there any way to replicate the behavior of multiple nested root elements and sections with a Message Element? If I create new pages/screens and try to transition that way, it rests the navigation controller and I lose the use of the back button, even if "push" is set to true.
Not sure what you want exactly. Replace your "Item Expanded" root element code with this to push a dialog viewcontoller on the navigation stack with a backbutton. Ofcourse your DialogViewcontroller should be in a UINavigation controller in the first place for this to work
Item.Tapped += delegate(DialogViewController arg1, UITableView arg2, NSIndexPath arg3)
{
var newDialogVC = new DialogViewController(
UITableViewStyle.Grouped,
new RootElement("Item Expanded")
{
new Section ("test2"){
new StringElement("Field Name", "test"),
new StringElement("Value", "test"),
new StringElement("Description", "test")
}
}
, true);
arg1.NavigationController.PushViewController(newDialogVC,true);
};

MonoTouch Dialog elements are not updating/repainting themselves

I have the following in a Section:
_favElement = new StyledStringElement (string.Empty);
_favElement.Alignment = UITextAlignment.Center;
if (_room.IsFavourite) {
_favElement.Image = UIImage.FromBundle ("Images/thumbs_up.png");
_favElement.Caption = "Unmark as Favourite";
} else {
_favElement.Image = null;
_favElement.Caption = "Mark as Favourite";
}
_favElement.Tapped += favElement_Tapped;
Then when I press the element I want the following to happen:
private void favElement_Tapped ()
{
if (_room.IsFavourite) {
_favElement.Image = null;
_favElement.Caption = "Mark as Favourite";
} else {
_favElement.Image = UIImage.FromBundle ("Images/thumbs_up.png");
_favElement.Caption = "Unmark as Favourite";
}
_room.IsFavourite = !_room.IsFavourite;
}
However the image and text does not change in the actual element when the element is tapped. Is there a refresh method or something that must be called? I've also tried changing the Accessory on Tapped as well and nothing changes. The properties behind do reflect the correct values though.
An alternative to reloading the UITableView is to reload the Element using code like this (copied from Touch.Unit):
if (GetContainerTableView () != null) {
var root = GetImmediateRootElement ();
root.Reload (this, UITableViewRowAnimation.Fade);
}
assuming that your code is in DialogViewController,add this
this.ReloadData();
but in your case I recommend you to use BooleanImageElement

Resizing MonoTouch.Dialog StyledMultilineElement after an async call

I'm playing with MonoTouch.Dialog and written some code to show some tweets. The problem is that the table cells are too small and the cells are all bunched up when I load the StyledMultilineElements asynchronously. They look absolutely perfect when I load them synchronously (i.e. without the QueueUserWorkItem/InvokeOnMainThread part)
Is there a way of getting the table cells to recalculate their height?
// This method is invoked when the application has loaded its UI and its ready to run
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
window.AddSubview(navigation.View);
var tweetsSection = new Section("MonoTouch Tweets"){
new StringElement("Loading...") //placeholder
};
var menu = new RootElement("Demos"){
tweetsSection,
};
var dv = new DialogViewController(menu) { Autorotate = true };
navigation.PushViewController(dv, true);
window.MakeKeyAndVisible();
// Load tweets async
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = true;
ThreadPool.QueueUserWorkItem(delegate {
var doc = XDocument.Load("http://search.twitter.com/search.atom?q=%23MonoTouch");
var atom = (XNamespace)"http://www.w3.org/2005/Atom";
var tweets =
from node in doc.Root.Descendants(atom + "entry")
select new {
Author = node.Element(atom + "author").Element(atom + "name").Value,
Text = node.Element(atom + "title").Value
};
var newElements =
from tweet in tweets
select new StyledMultilineElement(
tweet.Author,
tweet.Text);
InvokeOnMainThread(delegate {
UIApplication.SharedApplication.NetworkActivityIndicatorVisible = false;
tweetsSection.Remove(0);
tweetsSection.Add(newElements.Cast<Element>().ToList());
});
});
return true;
}
Try setting the UnevenRows property on your top level Root element of your Dialog View Controller, in this case "menu":
menu.UnevenRows = true

Help on Removal of Dynamically Created sprites

import flash.display.Sprite;
import flash.net.URLLoader;
var index:int = 0;
var constY = 291;
var constW = 2;
var constH = 40;
hydrogenBtn.label = "Hydrogen";
heliumBtn.label = "Helium";
lithiumBtn.label = "Lithium";
hydrogenBtn.addEventListener (MouseEvent.CLICK, loadHydrogen);
heliumBtn.addEventListener (MouseEvent.CLICK, loadHelium);
lithiumBtn.addEventListener (MouseEvent.CLICK, loadLithium);
var myTextLoader:URLLoader = new URLLoader();
myTextLoader.addEventListener(Event.COMPLETE, onLoaded);
function loadHydrogen (event:Event):void {
myTextLoader.load(new URLRequest("hydrogen.txt"));
}
function loadHelium (event:Event):void {
myTextLoader.load(new URLRequest("helium.txt"));
}
function loadLithium (event:Event):void {
myTextLoader.load(new URLRequest("lithium.txt"));
}
var DataSet:Array = new Array();
var valueRead1:String;
var valueRead2:String;
function onLoaded(event:Event):void {
var rawData:String = event.target.data;
for(var i:int = 0; i<rawData.length; i++){
var commaIndex = rawData.search(",");
valueRead1 = rawData.substr(0,commaIndex);
rawData = rawData.substr(commaIndex+1, rawData.length+1);
DataSet.push(valueRead1);
commaIndex = rawData.search(",");
if(commaIndex == -1) {commaIndex = rawData.length+1;}
valueRead2 = rawData.substr(0,commaIndex);
rawData = rawData.substr(commaIndex+1, rawData.length+1);
DataSet.push(valueRead2);
}
generateMask_Emission(DataSet);
}
function generateMask_Emission(dataArray:Array):void{
var spriteName:String = "Mask"+index;
trace(spriteName);
this[spriteName] = new Sprite();
for (var i:int=0; i<dataArray.length; i+=2){
this[spriteName].graphics.beginFill(0x000000, dataArray[i+1]);
this[spriteName].graphics.drawRect(dataArray[i],constY,constW, constH);
this[spriteName].graphics.endFill();
}
addChild(this[spriteName]);
index++;
}
Hi, I am relatively new to flash and action script as well and I am having a problem getting the sprite to be removed after another is called. I am making emission spectrum's of 3 elements by dynamically generating the mask over a picture on the stage. Everything works perfectly fine with the code I have right now except the sprites stack on top of each other and I end up with bold lines all over my picture instead of a new set of lines each time i press a button.
I have tried using try/catch to remove the sprites and I have also rearranged the entire code from what is seen here to make 3 seperate entities (hoping I could remove them if they were seperate variables) instead of 2 functions that handle the whole process. I have tried everything to the extent of my knowledge (which is pretty minimal # this point) any suggestions?
Thanks ahead of time!
My AS3 knowledge is rather rudimentary right now but I think two things may help you.
You could use removeChild before recreating the Sprite. Alternatively, just reuse the Sprite.
Try to add this[spriteName].graphics.clear(); to reset the sprite and start redrawing.
function generateMask_Emission (dataArray : Array) : void {
var spriteName:String = "Mask"+index;
trace(spriteName);
// Don't recreate if sprite object already created
if (this[spriteName] == null)
{
this[spriteName] = new Sprite();
// Only need to add sprite to display object once
addChild(this[spriteName]);
}
for (var i:int= 0; i < dataArray.length; i+=2)
{
this[spriteName].graphics.clear();
this[spriteName].graphics.beginFill(0x000000, dataArray[i+1]);
this[spriteName].graphics.drawRect(dataArray[i],constY,constW, constH);
this[spriteName].graphics.endFill();
}
index++;
}
Just in case anyone was curious or having a similar problem. Extremely simple fix but here is what I did.
Also should mention that I don't think that the graphics.clear function actually fixed the problem (though I didn't have the sprite being cleared properly before), but I believe the problem lies in the beginning of the onloaded function where 3 of those variables used to be outside of the function.
import flash.display.Sprite;
import flash.net.URLLoader;
import flash.events.Event;
var constY = 291; //this value represets the Y value of the bottom of the background spectrum image
var constW = 2; //this value represents the width of every emission line
var constH = 40; //this value represents the height of every emission line
//Create Button Labels
hydrogenBtn.label = "Hydrogen";
heliumBtn.label = "Helium";
lithiumBtn.label = "Lithium";
//These listen for the buttons to be clicked to begin loading in the data
hydrogenBtn.addEventListener (MouseEvent.CLICK, loadHydrogen);
heliumBtn.addEventListener (MouseEvent.CLICK, loadHelium);
lithiumBtn.addEventListener (MouseEvent.CLICK, loadLithium);
var myTextLoader:URLLoader = new URLLoader();//the object to load in data from external files
myTextLoader.addEventListener(Event.COMPLETE, onLoaded);//triggers the function when the file is loaded
var Mask:Sprite = new Sprite(); //This sprite will hold the information for the spectrum to be put on stage
function loadHydrogen (event:Event):void {
myTextLoader.load(new URLRequest("hydrogen.txt"));//starts loading Hydrogen emisson data
}
function loadHelium (event:Event):void {
myTextLoader.load(new URLRequest("helium.txt"));//starts loading Helium emission data
}
function loadLithium (event:Event):void {
myTextLoader.load(new URLRequest("lithium.txt"));//starts loading Lithium emission data
}
function onLoaded(event:Event):void {//the function that handles the data from the external file
var rawData:String = event.target.data; //create a new string and load in the data from the file
var DataSet:Array = new Array();//the array to load values in to
var valueRead1:String; //subset of array elements (n)
var valueRead2:String; //subset of array elements (n+1)
for(var i:int = 0; i<rawData.length; i++){ //loop through the string and cut up the data # commas
var commaIndex = rawData.search(",");
valueRead1 = rawData.substr(0,commaIndex);
rawData = rawData.substr(commaIndex+1, rawData.length+1);
DataSet.push(valueRead1);
commaIndex = rawData.search(",");
if(commaIndex == -1) {commaIndex = rawData.length+1;}
valueRead2 = rawData.substr(0,commaIndex);
rawData = rawData.substr(commaIndex+1, rawData.length+1);
DataSet.push(valueRead2);
}
generateMask_Emission(DataSet);//call the generateMaskEmission function on new data to fill emission lines
}
//This function loops through an array, setting alternating values as locations and alphas
function generateMask_Emission(dataArray:Array):void{
Mask.graphics.clear(); //Clears the Mask sprite for the next set of values
addChild(Mask); //Adds the blank sprite in order to clear the stage of old sprites
//This loop actually draws out how the sprite should look before it is added
for (var i:int=0; i<dataArray.length; i+=2){
Mask.graphics.beginFill(0x000000, dataArray[i+1]);
Mask.graphics.drawRect(dataArray[i],constY,constW, constH);
Mask.graphics.endFill();
}
addChild(Mask);// actually adds the mask we have created to the stage
}

Resources