I'm using a custom list adapter for a list view. I have defined a button in the list view and the click event works, but the problem is that once the list is scrolled, it binds multiple views with the same button. So on the click of the button, the event associated with each of the associated views is fired.
How do I deal with this?
I would guess that you are misunderstanding how the list works - especially how convertView's are used.
ListViews in Android virtualise the UI - just like ListBoxes do in WP and just like UITableViews do in iOS
What this means is that if the underlying list has 1000 items, but the screen only has room for 10 items, then the list will just create 10 'containers' to show list items, and will use those containers to display just the content that is in view at the time.
The way it does this is through the Adapter - and in particular through the GetView callback - which takes a convertView as one of its parameters.
If you choose to create a new view in your GetView implementation, then you can subscribe to new events in the callback...
If instead you choose to use the convertView in your GetView implementation, then you should not subscribe to new events in the callback - not without unsubscribing the old events first.
e.g. I'm guessing your code does something like this pseudo currently:
public View GetView(int pos, View convertView)
{
TextView toShow = convertView as TextView;
if (toShow == null)
{
toShow = new TextView();
}
toShow.Text = "Item at position " + i;
toShow.Click += (s,e) => {
// do something
};
return toShow;
}
The problem with the code is that you will subscribe to Click too often... you'd need to solve it with something like:
public View GetView(int pos, View convertView)
{
TextView toShow = convertView as TextView;
if (toShow == null)
{
toShow = new TextView();
toShow.Click += (s,e) => {
// do something with the position embedded in toShow.Tag
};
}
toShow.Text = "Item at position " + i;
toShow.Tag = new WrappedPosition(i);
return toShow;
}
That's my guess anyways :)
Stuart is completely right - problem is that views in ListView are reused (to avoid creating different objects), and as different parts of list are visible, for a new position you could get any view that is not used anymore. So your code should handle this properly.
I would like to add that Garbage Collection for Java objects in monodroid works not good. In my experience, creating lots of objects derived from Java.Lang.Object will crash the application. So:
Creating new View for each new row will soon crash the applicaion, so you have to reuse convertView whenever possible.
Tag has type Java.Lang.Object, so WrappedPosition should derive from Java object. This means that rather than creating new instance every time, you should reuse same instance.
If you move click handler to a separate method, you can just unsubscribe before subscribing, so you will not need any logic "if view is null".
If you find it useful, I may post here example of code that explains how it works. Dodn't post it initially as it's quite big :)
Related
I try to be more clear in titles... but... this is just weird...
1st: The setup
Working with Xamarin. I have 2 grid views. The 1st one is created and populated when the application opens. The 2nd one is created and populated when an item in the 1st grid view is clicked
Both gridviews are created with the same contructor and the same adapter classes.
2nd: The issue
The 1st thing I noticed was that when the second gridview was created, the 1st item of the 1st gridview would dissapear. Regardless of where I would click. With both grids visible the app looks like this:
Its easy to make it reappear, I just need to add a adapter.NotifyDatasetChanged() to the behaviour that makes the 1st cell dissapear and the cell never dissapers. Great... altough its a patch, not a solution, and this patch is not good enough when there's a lot of grid creation going on. Moreover, I also noticed that if I scroll down and back up, the cell would also reappear (likely because the view is forced to refresh the initial views when they come back to visible range). I figured my issue was the implementation of my custom adapter. Thus, I spent a decent amount of time looking at my GetView implementation. Then I noticed something weirder: If I scroll down and then back up the views refresh, just as I said before, however, if I only scroll a little, like halfway down the cell that is no longer visible, and then back up, this happens:
Yeah... it kinda keeps scrolling up forever. Is the problem still my adapter implementation?
My GetView is pretty simple, I have a separated list with views and the GetView fetchs the required view there:
public override View GetView (int position, View convertView, ViewGroup parent)
{
RelativeLayout temp;
if (convertView != null) {
temp = (RelativeLayout)convertView;
} else {
temp = new CustomGestureListener (Android.App.Application.Context);
}
temp.RemoveAllViews ();
RelativeLayout.LayoutParams cellLP = null;
if (data [position] != null) {
RelativeLayout referenceView = (RelativeLayout)data [position].NativeObj;
if(referenceView != null && referenceView.Handle != IntPtr.Zero){
View refParent = (View)referenceView.Parent;
if (refParent != null)
((ViewGroup)refParent).RemoveView (referenceView);
cellLP = new RelativeLayout.LayoutParams (RelativeLayout.LayoutParams.MatchParent, referenceView.LayoutParameters.Height);//(RelativeLayout.LayoutParams)referenceView.LayoutParameters;
temp.AddView (referenceView, cellLP);
}
}
return (View)temp;
}
I have been using the same adapter in my listviews without issues. Even if I replace the 1st grid by a Listview with this same adapter and I recreate this behaviour of creating a gridview, the issue does not happen in the ListView
I am interested in learning more about MVVM. I have taken a look at the MVVM Demo App. I understand many of the major concepts behind it.
When I began playing with the app, I wanted to open one of the tabviews by default upon the app starting up. However I am unsure on how to do that.
In the app, I think I understand that when a control panel button is clicked (e.g. View All Customers), the commandrelay creates a new AllCustomersViewModel and the data template applies the view to the viewmodel, the new workspace is created to the Workspaces collection and the tab opens because of the databinding in the main window.
I have no idea how to start this process other than clicking the hyperlink. I know that I need to call new RelayCommand(param => this.ShowAllCustomers()) but I don't understand how to call this without any user interaction, or how to call it from outside of the mainwindowviewmodel, e.g. from the app's onstartup method.
Can someone please advise on the best way to use a relaycommand on the start up of an app? Also, how do I use a relaycommand if the method I want to pass is within another class?
VMaleev has correctly given me a method to call the command, however the example provided was specific to a the collection of commands. What if I have a method Public ICommand HelpPageCommand which creates a new command based on a ShowHelpPage method where ShowHelpPage is;
HelpViewModel workspace = new HelpViewModel();
this.Workspaces.Add(workspace);
this.SetActiveWorkspace(workspace);
How would I call this command then?
- Simple, if the method is ICommand, then simply method.execute(null)
I suppose, you are talking about this article.
To call RelayCommand without user interaction, you just should write:
If want to call from MainWindowViewModel (for example, in constructor):
_commands.FirstOrDefault(q => q.DisplayName == Strings.MainWindowViewModel_Command_ViewAllCustomers).Command.Execute(null);
If want to call from App.xaml.cs (on application startup, code is taken from example and only one line added) or something else place where you have access to view model instance:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
MainWindow window = new MainWindow();
// Create the ViewModel to which
// the main window binds.
string path = "Data/customers.xml";
var viewModel = new MainWindowViewModel(path);
// When the ViewModel asks to be closed,
// close the window.
EventHandler handler = null;
handler = delegate
{
viewModel.RequestClose -= handler;
window.Close();
};
viewModel.RequestClose += handler;
// Allow all controls in the window to
// bind to the ViewModel by setting the
// DataContext, which propagates down
// the element tree.
window.DataContext = viewModel;
// the following line is added
viewModel.Commands.FirstOrDefault(q => q.DisplayName == Strings.MainWindowViewModel_Command_ViewAllCustomers).Command.Execute(null);
window.Show();
}
If the method you want to pass is in another class, there are two ways to do it:
pub/sub mechanism (by using c# events)
have access from every instance of view model to all instances of your view models. In this case you are able to pass method of any instance of view model as parameter of RelayCommand
Hope, it helps
I have been strugglin whit this problem for quite some time now. I'm building my first WPF MVVM application. In this App i have a AppView (with it's corresponding viewmodel). Child views are contained into tabs and represented by separated views (UserControl) and have one viewmodel for each view. So far so good.
In one view, a have a list of costumers, and a Delete button. I also have a correspondig command on the viewmodel to actualy delete the record, and this work fine. Now I want the delete button to create a new view with two buttons, one for confirmation and the other for cancel, and then if user click the "Confirm" button execute the delete.
The problem here is that each view, and its correspondig viewmodel are isolated from the other (as long as I understand) so i cannot access the second view viewmodel to see if the confirm button is clicked.
The only posible solution that i found so far is to add an event on one view and subscribe the other view to that event. But this technic is quite complex for such a trivial task. Is there other alternatives? Can't the two views share the same datacontext or viewmodel?
Thanks!
var dialog = new DialogViewModel();// could be a DialogService if you wish
with in this DialogViewModel or DialogService again your choice how you actually do it.
dialog.Result which in this case would return your confirmation either true or false
var settings = new Dictionary<string, object>();
settings["Owner"] = this;
settings["WindowStartupLocation"] = WindowStartupLocation.CenterParent;
windowManager.ShowDialog(dialog, null, settings);
if(dialog.Result == true)
do the delete on the parent viewmodel.
Or you can do it all with IEventAggregator and a message package. I personally use the first for a lot of things. Sometimes a combination depending on situation.
Most will favor the IDialogService method of things for SoC, and do DI with it to bring it into the viewmodel using it. Then each viewmodel will be responsible its own dialogs. From there you can call ShowDialog since its part of the WindowManager, which you click Yes or No, or what ever you setup for you dialogview. Numerous ways to skin the cat but in the end you want KISS methodology and something that won't break the patterns you are trying to adhere too.. Hell for all it matters you could add it to a viewmodelbase base class for all of your viewmodels to inherit to access globally. All a function how you want your app to behave in the end anyway.
--update--
public class YourViewModel(IWindowManager winMan)
{
private readonly IWindowManager _winMan;
public YourViewModel()
{
_winMan = winMan;
}
public void DeleteCustomer()
{
var dialog= new DialogViewModel(); // not best way but...
var settings = new Dictionary<string, object>();
settings["Owner"] = this; //<< Parent
settings["StartupLocation"] = WindowStartupLocation.CenterParent;
_winMan.ShowDialog(dialog, null, settings);
if(dialog.Result)
//do delete
else
//do nothing
}
}
In the MvvmCross N=26 tutorial, dynamic fragments are loaded into a frame via button click event in the View (code snippet below). However, I'm trying to figure out how handle the click event in the ViewModel and not in the View. After the button is clicked, how do I know the button was clicked and in the View, load the correct fragment in the frame?
For instance, I may have 10 fragments and one frame in the FirstView xml. I want to be able to load any of those 10 fragments in that frame based on a property of a object referenced in the FirstViewModel. Can I check that property in the View and load the fragment that I want from the 10 fragments available? (i.e. remove the but1.Click event in the View and still run the transaction based on the value of the object in the ViewModel)
but1.Click += (sender, args) =>
{
var dNew = new DubFrag()
{
ViewModel = ((SecondViewModel) ViewModel).Sub
};
var trans3 = SupportFragmentManager.BeginTransaction();
trans3.Replace(Resource.Id.subframe1, dNew);
trans3.AddToBackStack(null);
trans3.Commit();
};
The approach you suggest of mapping a vm property to which fragment to show should work, yes.
To use this, just subscribe to property changed in your view code (there are some weak reference helper classes and extension methods to assist with this)
Alternatively, this blog post - http://enginecore.blogspot.ca/2013/06/more-dynamic-android-fragments-with.html?m=1 - introduces a mini framework that allows navigating by fragments.
A similar approach is used in the Shakespeare sample in the mvvmcross-tutorials fragments sample.
It should be possible to adapt that code to your needs
In my Silverlight project I am creating textboxes which are two-way databound to some Context during runtime. The binding in one direction (from the source to the target) seems to work fine, but the other direction (from the target back to the source) is not showing any effect.
This is the data-context:
public class Leg : INotifyPropertyChanged {
private string passengers;
public string Passengers {
get { return passengers; }
set {
// here I have a breakpoint.
passengers = value;
FirePropertyChanged("Passengers");
}
}
private void FirePropertyChanged (string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Then on another place I am creating a new TextBox control together with a binding for it:
Binding passengersBinding = new Binding();
// viewModelLeg is an instance of the class Leg from above
passengersBinding.Source = viewModelLeg;
passengersBinding.Path = new PropertyPath("Passengers");
passengersBinding.Mode = BindingMode.TwoWay;
legItem.paxTextBox.SetBinding(TextBox.TextProperty, passengersBinding);
Now when I am altering the value of the Passengers string the corresponding textbox that is bound to it is updating its text correctly. So here's everthing fine.
But when i change the text of a textbox manually and then make the textbox lose its focus, nothing happens - i.e. there is no two-way binding taking place - no down propagation of the new text-value of the textbox to the source !
I have a breakpoint at the setter of the passengers-attribute (marked with the breakpoint-comment above). When I am getting all this right the binding engine also uses this public setter when the target-value of a binding has changed to update the source - so when this happens the breakpoint must be hit. But he doesn't ! So it seems that i can do what I want with my textbox (play with the focus or press enter) it is never updating its source.
Am I overseeing something ? There must be a capital error either in my code or in my thinking.. i would be really thankful for any ideas ...
EDIT:
In the following I try to demonstrate how i create my XAML objects and my DataContext objects. Because I am creating XAML controls and their bindings at runtime I haven't found a good solution to implement the MVVM approach very well. So I am doing the following (which is maybe not the best way to do it):
The situation I am modelling is that I have a UserControl (called LegItem) which is comprised (primarely) of textboxes. At runtime the user can create as much of these userControls as hew wishes to (one after the other).
On my ViewModel side I have a class (called Leg) that serves as a ViewModel for exactly one LegItem. So when I have say n (XAML-) LegItems then I also have n Leg instances. I store these Leg objects in a List.
So I am doing the following everytime the user clicks the 'add a new leg' button:
// here we are inside the applications view in an .xaml.cs file
public void AddLeg () {
// this is going to serve as the ViewModel for the new LegItem
// I am about to create.
Leg leg = viewModel.insertLeg();
// here I am starting to create the visual LegItem. The ViewModel object
// I have created in the previous step is getting along with.
createLegItem(leg);
}
// the primary job here is to bind each contained textbox to its DataContext.
private LegItem createLeg (Leg viewModelLeg) {
// create the visual leg item control element
// which is defined as a XAML UserControl.
LegItem legItem = new LegItem();
Binding passengersBinding = new Binding();
// viewModelLeg is an instance of the class Leg from above
passengersBinding.Source = viewModelLeg;
passengersBinding.Path = new PropertyPath("Passengers");
passengersBinding.Mode = BindingMode.TwoWay;
legItem.paxTextBox.SetBinding(TextBox.TextProperty, passengersBinding);
}
// on the viewModel side there is this simple method that creates one Leg object
// for each LegItem the View is creating and stores inside a simple list.
public Leg InsertLeg () {
Leg leg = new Leg();
legList.add(leg)
return leg;
}
New Answer
Since you mentioned your binding was actually to a custom UserControl and not actually a TextBox, I would suggest looking into the XAML of your UserControl and making sure it is binding the data correctly
Old Answer
I did a quick test with a new Silverlight project and noticed that the startup project is SilverlightApplication1.Web, not SilverlightApplication.
This means that the breakpoint in the setter won't actually get hit when I run the project. You'll notice the breakpoint circle is just the outline, and the color isn't filled in. If you hover over it, it will say
The breakpoint will not currently be hit. No symbols have been loaded
for this document
If I start SilverlightApplication1 instead of the .Web version, the breakpoint gets hit.
The property is getting changed correctly regardless of which version I startup, however the breakpoint isn't getting hit if I start the project with the .Web version. I suspect this is your issue.