Monotouch.Dialog: crash with EnableSearch and custom RootElement - search

i use Monotouch.Dialog (Xamarin.iOS Version: 6.2.0.65) to create a RadioGroup with RadioElements. I only want the 'search' functionality in the second screen (where the user selects from the long list), so i created a separate DialogViewController where i enabled the search filter (enableSearch = true).
When typing in the search box, the app crashes in the GetCell method of the RadioElement.
"Object reference not set to an instance of an object" on line 1064 of Element.cs. Do i have GC problems? I managed to simulate it in a small app... Don't pay attention some weird field members, thats me testing if i'm begin GC...
using MonoTouch.Dialog;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
namespace SearchCrash
{
// The UIApplicationDelegate for the application. This class is responsible for launching the
// User Interface of the application, as well as listening (and optionally responding) to
// application events from iOS.
[Register ("AppDelegate")]
public partial class AppDelegate : UIApplicationDelegate
{
// class-level declarations
UIWindow window;
public UINavigationController NavigationController { get; set; }
//
// This method is invoked when the application has loaded and is ready to run. In this
// method you should instantiate the window, load the UI into it and then make the window
// visible.
//
// You have 17 seconds to return from this method, or iOS will terminate your application.
//
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
window = new UIWindow(UIScreen.MainScreen.Bounds);
this.NavigationController = new UINavigationController();
this.NavigationController.PushViewController(new FirstViewController(this.NavigationController), true);
window.RootViewController = this.NavigationController;
window.MakeKeyAndVisible();
return true;
}
}
public class FirstViewController : DialogViewController
{
private UINavigationController controller;
private LocatiesRootElement locRootElement;
private RadioGroup radioGroup;
public FirstViewController(UINavigationController c) : base(new RootElement("Test"), true)
{
this.controller = c;
}
public override void ViewDidLoad()
{
Section section = new Section();
radioGroup = new RadioGroup(0);
locRootElement = new LocatiesRootElement("test", radioGroup, this.controller);
section.Add(locRootElement);
Root.Add(section);
}
}
public class LocatiesRootElement : RootElement
{
private UINavigationController navigationController;
private LocatiesViewController locatiesViewController;
public LocatiesRootElement(string selectedLocatie, RadioGroup group, UINavigationController controller) : base("Locatie", group)
{
this.navigationController = controller;
this.locatiesViewController = new LocatiesViewController(this);
}
public override void Selected(DialogViewController dvc, UITableView tableView, NSIndexPath path)
{
this.navigationController.PushViewController(this.locatiesViewController, true);
}
}
public class LocatiesViewController : DialogViewController
{
public LocatiesViewController(LocatiesRootElement root) : base(root, true)
{
this.EnableSearch = true;
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
Section sectionList = new Section();
RadioElement radioElement1 = new RadioElement("item 1");
RadioElement radioElement2 = new RadioElement("item 2");
sectionList.Add(radioElement1);
sectionList.Add(radioElement2);
Root.Add(sectionList);
}
}
}

Try enabling the search after you instantiate the DialogViewController.
Like this :
public LocatiesRootElement(string selectedLocatie, RadioGroup group, UINavigationController controller) : base("Locatie", group)
{
this.navigationController = controller;
this.locatiesViewController = new LocatiesViewController(this);
this.locatiesViewController.EnableSearch = true;
}
Hopefully that will work for you.

The bug report here includes a workaround for the root cause of the issue you're experiencing, but also talks about how filtering will then cause a usability issue of marking the nth element as selected even after a filter has been applied.
https://github.com/migueldeicaza/MonoTouch.Dialog/issues/203
If you don't want to update the core MTD code, you could use that same technique by putting it in your own UIBarSearchDelegate. Unfortunately, the default SearchDelegate class is internal, so you'll need to add all of the code in your delegate. I was able to do this and get it working without changing the MTD source:
public override void LoadView()
{
base.LoadView();
((UISearchBar)TableView.TableHeaderView).Delegate = new MySearchBarDelegate(this);
}
And then you use this instead of the base method:
public override void TextChanged (UISearchBar searchBar, string searchText)
{
container.PerformFilter (searchText ?? "");
foreach (var s in container.Root)
s.Parent = container.Root;
}

Related

Show Master on UISplitViewController by Default

When I create a new "Master Detail App" with Visual Studio for Mac (v8.4.5), the default behavior of the UISplitViewController is to show the Detail page first when it appears on an iPhone in Portrait mode.
I would rather (as I think most people would rather) have the Master page show by default. In my case, the Master page is a table view that holds a list of contacts.
This question is similar to: UISplitViewController in portrait on iPhone shows detail VC instead of master but for Xamarin.iOS
Similar to the solutions suggested there, I have attempted to assign a delegate without success:
public class ContactsSplitViewControllerDelegate : UISplitViewControllerDelegate
{
public override bool EventShowViewController(UISplitViewController splitViewController, UIViewController vc, NSObject sender)
{
return true;
}
public override bool EventShowDetailViewController(UISplitViewController splitViewController, UIViewController vc, NSObject sender)
{
return true;
}
}
public partial class ContactsSplitViewController : UISplitViewController
{
public ContactsSplitViewController (IntPtr handle) : base (handle)
{
this.Delegate = new ContactsSplitViewControllerDelegate();
}
}
set the PreferredDisplayMode
public override void ViewDidLoad()
{
base.ViewDidLoad();
this.PreferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible;
}
After some experimentation, it seems that overriding CollapseSecondViewController on the delegate will work though I'm not yet convinced it's the proper solution.
using Foundation;
using System;
using UIKit;
namespace MasterDetailTest
{
public class SplitViewControllerDelegate : UISplitViewControllerDelegate
{
public override bool CollapseSecondViewController(UISplitViewController splitViewController, UIViewController secondaryViewController, UIViewController primaryViewController)
{
return true;
}
}
public partial class MainPageSplitViewController : UISplitViewController
{
public MainPageSplitViewController (IntPtr handle) : base (handle)
{
this.Delegate = new SplitViewControllerDelegate();
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
// When implemented in my project, I found I needed to set this
// or the delegate would not be called.
this.SetNeedsFocusUpdate();
}
}
}

Xamarin iOS: How to hide seek bar in AvPlayer?

I'm building an app in xamarin ios using AvPlayer. How can I hide the seek bar ?
_playerViewController = new CustomAVPlayerViewController();
_player = new AVPlayer();
_playerViewController.Player = _player;
SetNativeControl(_playerViewController.View);
public class CustomAVPlayerViewController: AVPlayerViewController
{
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
}
}
If you don't want to show the controls or want to highly customize the appearance, I recommend you to use AVPlayerLayer:
public class CustomPlayerRenderer : ViewRenderer<CustomPlayer, UIView>
{
AVPlayer _player;
AVPlayerLayer _playerLayer;
protected override void OnElementChanged(ElementChangedEventArgs<CustomPlayer1> e)
{
base.OnElementChanged(e);
if (Control == null)
{
_player = new AVPlayer(new NSUrl(NSBundle.MainBundle.PathForResource("sample.mp4", null), false));
_playerLayer = AVPlayerLayer.FromPlayer(_player);
UIView view = new UIView();
view.Layer.AddSublayer(_playerLayer);
SetNativeControl(view);
//_player.Play();
// Add custom controls as you want on this view
}
}
public override void Draw(CGRect rect)
{
base.Draw(rect);
_playerLayer.Frame = rect;
}
}

Xamarin CollectionViewCell not instantiating from storyboard

I am collaborating with a designer who styles elements on my Xamarin project storyboard. I have trouble referencing his carefully placed elements in code. All the outlets are created, but in a UICollectionViewCell the UILabels etc are not instantiated. Here is code from a simple test app to demonstrate the problem.
The Xamarin generated code-behind:
[Register ("CardCell")]
partial class CardCell
{
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UILabel txtName { get; set; }
void ReleaseDesignerOutlets ()
{
if (txtName != null) {
txtName.Dispose ();
txtName = null;
}
}
}
My attempt to set the UI properties which causes the crash:
public partial class CardCell : UICollectionViewCell
{
public CardCell (IntPtr handle) : base (handle)
{
}
public void Update(string name)
{
txtName.Text = name; // throws exception here, because textName is null
}
}
The view controller with delegate methods:
public partial class TestCollectionController : UICollectionViewController
{
static NSString cardCellId = new NSString ("cardcell");
string[] cards = new string[] {"Red", "Green", "White", "Blue", "Pink", "Yellow"};
public TestCollectionController (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
CollectionView.RegisterClassForCell (typeof(CardCell), cardCellId);
}
public override nint NumberOfSections (UICollectionView collectionView)
{
return 1;
}
public override nint GetItemsCount (UICollectionView collectionView, nint section)
{
return (nint)cards.Length;
}
public override UICollectionViewCell GetCell (UICollectionView collectionView, NSIndexPath indexPath)
{
CardCell cell = (CardCell)collectionView.DequeueReusableCell (cardCellId, indexPath);
var card = cards [indexPath.Row];
cell.Update(card);
return cell;
}
}
I have tried both the Xamarin iOS Designer and Xcode's Interface Builder, but it does not make a difference. Any help will be greatly appreciated.
Examples of collection views driven by a storyboard are quite scarce, but I found this one and teased through it meticulously till I discovered that it did not include the call to register a class for the cell. So, remove this line:
CollectionView.RegisterClassForCell (typeof(CardCell), cardCellId);
and suddenly everything works as expected.
I was following this recipe and other examples, which all had the call included, which is only needed when you don't use a storyboard.

How to update ListView from completely unrelated class

I've read through a lot of questions on how to update a ListView. They all pretty much say adapter.notifyDataSetChanged() (if in a seperate thread with runOnUiThread).
My problem starts a bit earlier: How do I even get the Adapter I need or for that matter an Activity to call runOnUiThread?
Here is a very simplified version of my current code:
I have a class for the Data, which can be updated from anywhere at any time. The update method needs to start a new Thread, which is why I can't simply wait for a return value.
class Data {
private static double[] data = new double[7]
public static void update(final Context context) {
new Thread(new Runnable() {
#Override
public void run() {
//do lots of complicated stuff
}
}).start();
}
public double[] getData {
return data;
}
I have an Activity (not the main Activity) that, among other stuff, contains the ListView
public class ListViewActivity extends ActionBarActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ListView);
ListView listView = (ListView) findViewById(R.id.MyListView);
listView.setAdapter(new CustomListViewAdapter(this));
}
I then use a custom Adapter to set up the ListView how I want it.
class CustomListViewAdapter extends BaseAdapter {
private final LayoutInflater layoutInflater;
public CustomListViewAdapter(Context context) {
layoutInflater = LayoutInflater.from(context);
}
#Override
public View getView(int position, View view, ViewGroup parent) {
ViewHolder viewHolder = new ViewHolder();
if (view == null) {
view = layoutInflater.inflate(R.layout.listView_row_layout, parent, false);
viewHolder.value = (TextView) view.findViewById(R.id.listViewDataTextField);
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
}
double data = Data.getData()[position];
viewHolder.value.setText(data+"");
return view;
}
private class ViewHolder {
TextView value;
}
Now I want to update the ListView with as soon as the Thread in the update() method has finished getting the new data. Getting the data can take several seconds, so it's impossible to predict, what activity will be active by that time. If my ListViewActivity is active, I want the ListView to change to the new data immediately.
So how do I update that ListView, from a different class, which is not an Activity, inside another thread? I don't suppose putting the adapter in a static field is a good idea, since that ListViewActivity is probably created and destroyed all the time? But what other options do I have?
Define a callback interface. Make your update method take an instance of this callback as an argument and call back to it when updating has finished.
public interface UpdateCallback {
public void onUpdateFinished();
}
public void update(final Context context, final UpdateCallback callback) {
new Thread(new Runnable() {
#Override
public void run() {
// do lots of complicated stuff
callback.onUpdateFinished();
}
}).start();
}
Then in your Activity you can notify that the adapter data has changed:
Data.update(this, new UpdateCallback() {
public void onUpdateFinished() {
adapter.notifyDatasetChanged();
}
});
extendIt looks like your major problem can be formulated shortly as "How do I even get the Adapter", so I'll try to address that. Let us try to use an old good Java singleton to solve the problem. The idea is to create a global config and pre-create adapters for all views that you need to update:
public class GlobalConfig {
private static GlobalConfig config = null;
private CustomBaseAdapterOne adapter1 = null;
private CustomBaseAdapterTwo adapter2 = null;
public GlobalConfig getAdapterOne() {return adapter1;}
public GlobalConfig getAdapterTwo() {return adapter2;}
public static synchronized GlobalConfig getInstance() {
if (config == null) {
config = new GlobalConfig();
config.adapter1 = new CustomBaseAdapterOne();
config.adapter2 = new CustomBaseAdapterTwo();
}
return config;
}
}
Your ListViewActivity will need to extend DataSetObserver class and register an observer when it starts. You CustomListAdapter would need to register the observer when it starts and it will also need to be able to get initialized with init params (Context in your case) through a function (e.g. setContext), not through a constructor, so in your ListViewActivity you'll need to do something like below:
GlobalConfig cfg = GlobalConfig.getInstance();
CustomerAdapterOne ad = cfg.getAdapterOne();
ad.setContext(this);
ad.registerDataSetObserver(this);
From a separate thread that needs to update the ListView, you'll run:
GlobalConfig cfg = GlobalConfig.getInstance();
try {
cfg.getAdapterOne().notifyDataSetChanged();
}
catch{... }
I think, it's a good idea to have try/catch here because I don't know what will happen if ListView has already died at the time when you need to notify.
To summarize: you'll need to pre-create one adapter for each view that needs to be updated and make it globally available for everyone who needs it.

UICollectionView - Curious flow layout

I'm experiencing with the UICollectionView(Controller) for the first time. Actually it should be as simple as working with TableViews, it's not though.
Rather than showing all images in a flow (several rows) just the top row is displayed. All the other images are somewhere... scrolling is enabled but nothing happens, no bouncing, no scrolling, ...
And after an orientation change (and back) some more images are visible but they appear randomly. After every orientation change they appear at an other location.
My example should have 7 images.
My properties in IB:
First time:
After rotating (and back):
And my source code to implement the photo gallery.
using System;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
using System.Collections.Generic;
using Xamarin.Media;
using MonoTouch.AssetsLibrary;
using MonoTouch.CoreGraphics;
using System.Diagnostics;
using System.Linq;
using System.Drawing;
namespace B2.Device.iOS
{
public partial class TagesRapportDetailRegieBilderCollectionViewController : UICollectionViewController
{
private const string Album = "Rapport";
public TagesRapportDetailRegieBilderSource Source { get; private set; }
private TagesRapportDetailRegieBilderDelegate _delegate;
public TagesRapportDetailRegieBilderCollectionViewController (IntPtr handle) : base (handle)
{
Source = new TagesRapportDetailRegieBilderSource(this);
_delegate = new TagesRapportDetailRegieBilderDelegate(this);
// Delegate - Muss im konstruktor sein. ViewDidLoad geht nicht!
CollectionView.Delegate = _delegate;
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
// Cell Klasse registrieren
CollectionView.RegisterClassForCell (typeof(ImageCell), new NSString("imageCell"));
// DataSource
CollectionView.Source = Source;
// Bilder laden
LoadImages();
}
private void LoadImages()
{
Source.Images.Clear();
var assetsLibrary = new ALAssetsLibrary();
assetsLibrary.Enumerate(ALAssetsGroupType.Album, GroupsEnumeration, GroupsEnumerationFailure);
}
private void GroupsEnumeration(ALAssetsGroup group, ref bool stop)
{
if (group != null && group.Name == Album)
{
//notifies the library to keep retrieving groups
stop = false;
//set here what types of assets we want,
//photos, videos or both
group.SetAssetsFilter(ALAssetsFilter.AllPhotos);
//start the asset enumeration
//with ALAssetsGroup's Enumerate method
group.Enumerate(AssetsEnumeration);
CollectionView.ReloadData();
}
}
private void AssetsEnumeration(ALAsset asset, int index, ref bool stop)
{
if (asset != null)
{
//notifies the group to keep retrieving assets
stop = false;
//use the asset here
var image = new UIImage(asset.AspectRatioThumbnail());
Source.Images.Add(image);
}
}
private void GroupsEnumerationFailure(NSError error)
{
if (error != null)
{
new UIAlertView("Zugriff verweigert", error.LocalizedDescription, null, "Schliessen", null).Show();
}
}
}
public class TagesRapportDetailRegieBilderDelegate : UICollectionViewDelegateFlowLayout
{
private TagesRapportDetailRegieBilderCollectionViewController _controller;
public TagesRapportDetailRegieBilderDelegate(TagesRapportDetailRegieBilderCollectionViewController controller)
{
_controller = controller;
}
public override System.Drawing.SizeF GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath)
{
var size = _controller.Source.Images[indexPath.Row].Size.Width > 0
? _controller.Source.Images[indexPath.Row].Size : new SizeF(100, 100);
size.Width /= 3;
size.Height /= 3;
return size;
}
public override UIEdgeInsets GetInsetForSection(UICollectionView collectionView, UICollectionViewLayout layout, int section)
{
return new UIEdgeInsets(50, 20, 50, 20);
}
}
public class TagesRapportDetailRegieBilderSource : UICollectionViewSource
{
private TagesRapportDetailRegieBilderCollectionViewController _controller;
public List<UIImage> Images { get; set; }
public TagesRapportDetailRegieBilderSource(TagesRapportDetailRegieBilderCollectionViewController controller)
{
_controller = controller;
Images = new List<UIImage>();
}
public override int NumberOfSections(UICollectionView collectionView)
{
return 1;
}
public override int GetItemsCount(UICollectionView collectionView, int section)
{
return Images.Count;
}
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
var cell = collectionView.DequeueReusableCell(new NSString("imageCell"), indexPath) as ImageCell;
cell.Image = Images[indexPath.Row];
return cell;
}
}
public class ImageCell : UICollectionViewCell
{
UIImageView imageView;
[Export ("initWithFrame:")]
public ImageCell (System.Drawing.RectangleF frame) : base (frame)
{
imageView = new UIImageView(frame);
imageView.AutoresizingMask = ~UIViewAutoresizing.None;
ContentView.AddSubview (imageView);
}
public UIImage Image
{
set
{
imageView.Image = value;
}
}
}
}
If all you want to do is displaying the cells in an uniform grid, overriding GetSizeForItem shouldn't be necessary in the first place. Just configure the cellsize property of your flow layout either in IB or programatically during ViewDidLoad and be done with it.
There's another problem with your code:
group.Enumerate(AssetsEnumeration)
This will run asynchronously. That means:
CollectionView.ReloadData();
Will only cover a small subset of your images. It would be better to issue a ReloadData() when group == null, which indicates that the enumeration is complete.
You could also avoid ReloadData alltogether and call CollectionView.InsertItem() everytime you've added an image. This has the benefit of your items become immediately visible instead of all of them at once after everything has been enumerated - which may take some time (on the device). The downside is that you've got to be careful to not run into this.

Resources