Xamarin custom multiple choice listview - c#

I was wondering if there is a way to make a multi choice list view that is actually able to return the selected indexes. I've been able to do it with the pre-made multiplechoicelistview adapter but I need to be able to edit the style of it. So I need a custom listview.
This is my oncreate code a
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.Options);
outList = FindViewById<ListView>(Resource.Id.outList);
var btnCheck = FindViewById<ImageButton>(Resource.Id.btnConfirm);
var btnBack = FindViewById<ImageButton>(Resource.Id.btnBack);
for (int i = 0; i < NewProfileVars.LifeStyles.Length; i++)
{
inList.Add(NewProfileVars.LifeStyles[i].Name);
}
//list contents end here
ListViewAdapter adapter = new ListViewAdapter(this, inList);
outList.Adapter = adapter;
outList.ChoiceMode = ChoiceMode.Multiple;
NewProfile main = new NewProfile();
btnCheck.Click += Confirm;
btnBack.Click += Back;
}
And here is my list view adaptor code
class ListViewAdapter: BaseAdapter<string>
{
public List<string> Items;
public Context Context;
public ListViewAdapter(Context context, List<string> items)
{
Items = items;
Context = context;
}
public override int Count
{
get { return Items.Count; }
}
public override long GetItemId(int position)
{
return position;
}
public override string this[int position]
{
get { return Items[position]; }
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
View row = convertView;
if (row == null)
{
row = LayoutInflater.From(Context).Inflate(Resource.Layout.ListBox, null, false);
}
CheckBox txtName = row.FindViewById<CheckBox>(Resource.Id.cbName);
txtName.Text = Items[position];
return row;
}
}
All I need now is to figure out how that confirm button would save the things I have selected.
Thank you in advanced for the help.

I see you are using CheckBox in your ListView. You could get the Items that where Checked using something like this:
First create a class that will hold your item data and the Checked state, for the example let's call it
public class LifeStylesListItem
{
public string Name { get; set; }
public bool IsSelected { get; set; }
public LifeStylesListItem(string name)
{
Name = name;
}
}
Then modify your ListViewAdapter
Add a new private field that will hold a list of LifeStylesListItem
private List<LifeStylesListItem> _list;
Initialize the list with the Items passed in the constructor.
public ListViewAdapter(Context context, List<string> items)
{
Items = items;
_list = new List<LifeStylesListItem>();
//Your are creating a copy of your Items
foreach (var item in items)
{
_list.Add(new LifeStylesListItem(item));
}
Context = context;
}
In the GetView method subscribe to the CheckedChange event of your CheckBox. This way you will be notify when it's checked state has changed. Also you need to set the Checked Property based on the Item IsSelected value. This is necessary when the ListView will reuse your cell.
public override View GetView(int position, View convertView, ViewGroup parent)
{
View row = convertView;
if (row == null)
{
row = LayoutInflater.From(Context).Inflate(Resource.Layout.ListBox, null, false);
}
CheckBox txtName = row.FindViewById<CheckBox>(Resource.Id.cbName);
txtName.Text = _list[position].Name;
txtName.Checked = _list[position].IsSelected;
txtName.CheckedChange -= TxtName_CheckedChange;
txtName.CheckedChange += TxtName_CheckedChange;
return row;
}
Add the event handler TxtName_CheckedChange method
void TxtName_CheckedChange(object sender, CompoundButton.CheckedChangeEventArgs e)
{
//These lines are used to get the position of the control that was clicked
var obj = sender as CheckBox;
var row = obj?.Parent as View;
var parent = row?.Parent as ListView;
if (parent == null)
{
return;
}
var position = parent.GetPositionForView(row);
// Once you have the position you can get the item and change
// its IsSelected
var item = _list[position];
item.IsSelected = e.IsChecked;
}
Then a last method to add in the Adapter is the one that will return the selected Items. With the help of Linq (using System.Linq needs to be added) you can query the selected items like this.
public List<string> GetCheckedItems()
{
return _list
.Where(a => a.IsSelected)
.Select(b => b.Name)
.ToList();
}
Now in your Activity you just need to call the GetCheckedItems from the ListViewAdapter method on the Confirm button click:
private void Confirm(object sender, EventArgs e)
{
var checkedItems = adapter.GetCheckedItems();
}
Remember to change adapter as private field in your Activity
private ListViewAdapter adapter;
Hope this helps.-

Related

Remove the line below the selected item in a listbox

When I add an item into a listbox I also add a new line because I want there to be a blank line between each item added. When I remove a selected item I also want to remove the blank line I added otherwise I will end up getting 2 blank lines between each item this is the problem I am having so I thought if I could delete the selected item as well as the blank line above and below the selected item this would work. Is there a better approach to this?
ListBox1.Items.Remove(ListBox1.SelectedItem);
I have typed the items and differentiate what is the blank item and what is the value item. At the time of deleting I have the reference of both. It worked fine, see if it helps.
Here's an example:
Form:
private void Form1_Load(object sender, EventArgs e)
{
Data data = new Data { description = "Test1" };
listBox1.Items.Add(data);
data.BlankLine = new BlankItem();
listBox1.Items.Add(data.BlankLine);
data = new Data { description = "Test2" };
listBox1.Items.Add(data);
data.BlankLine = new BlankItem();
listBox1.Items.Add(data.BlankLine);
data = new Data { description = "Test3" };
listBox1.Items.Add(data);
data.BlankLine = new BlankItem();
listBox1.Items.Add(data.BlankLine);
data = new Data { description = "Test4" };
listBox1.Items.Add(data);
data.BlankLine = new BlankItem();
listBox1.Items.Add(data.BlankLine);
}
Event to delete the item on click:
private void listBox1_Click(object sender, EventArgs e)
{
if((listBox1.SelectedItem != null && listBox1.SelectedItem.GetType() != typeof(BlankItem)))
{
Data item = (Data)listBox1.SelectedItem;
listBox1.Items.Remove(item);
listBox1.Items.Remove(item.BlankLine);
}
}
Object Data
public class Data
{
public string description { get; set; }
public BlankItem BlankLine { get; set; }
public override string ToString()
{
return description;
}
}
Object BlankItem
public class BlankItem
{
public override string ToString()
{
return Environment.NewLine;
}
}
I wanted to try to implement the above functionality, but using a data-bound Listbox such that I make changes to the underlying list instead of the Listbox. If possible, use BindingList<T> instead of List<T> because it implements additional functionality specific to data binding.
The core is still the same, as each item added must also be followed by adding a string.Empty item. The same for removal, when an item is removed
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
var list = new BindingList<string>();
list.Add("ABC");
list.Add(string.Empty);
list.Add("GHK");
list.Add(string.Empty);
list.Add("OPQ");
listBox1.DataSource = list;
var binding = listBox1.BindingContext[list] as CurrencyManager;
listBox1.KeyDown += (s, ev) =>
{
if (ev.KeyData == Keys.Delete)
{
if (listBox1.SelectedItem != null && !listBox1.SelectedItem.Equals(string.Empty))
{
int index = listBox1.SelectedIndex;
if (index >= 0)
{
list.RemoveAt(index);
if (index < list.Count && list[index].Equals(string.Empty))
{
list.RemoveAt(index);
}
binding.Refresh();
}
}
}
if (ev.KeyData == Keys.Insert)
{
int index = listBox1.SelectedIndex;
if (index==-1 || list[index] == string.Empty)
{
index++;
}
list.Insert(index, "NEW " + (index + 1).ToString());
list.Insert(index+1, string.Empty);
}
};
}
}
press the [DEL] key to remove an item, and the [INS] key to add an item.
But I am not happy with this solution. I think there is a way to create a class that implements IListSource that you directly add/remove items and it creates a list with blanks in between automatically for binding.

Android - Turn switch "On" on ListView item click

I have a custom listview in my C# Android app, each row contains a textview, ImageView and a switch. When a Listview item is clicked, I want to turn the row's item switch on.
MainActivity:
List<TableList> list = = new List<TableList>();
list.Add(new TableList("Germany"));
list.Add(new TableList("France"));
list.Add(new TableList("Finland"));
listView.ItemClick += delegate (object sender, AdapterView.ItemClickEventArgs e)
{
string selected = t.Name;
if (selected == "France")
{
// Turn the proper switch for France row ON
}
};
ListAdapter and ListClass for the Listview:
public class ListAdapter : BaseAdapter<TableList>
{
List<TableList> items;
Activity context;
public ListAdapter(Activity context, List<TableList> items)
: base()
{
this.context = context;
this.items = items;
}
public override long GetItemId(int position)
{
return position;
}
public override TableList this[int position]
{
get { return items[position]; }
}
public override int Count
{
get { return items.Count; }
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = items[position];
View view = convertView;
if (view == null) // no view to re-use, create new
view = context.LayoutInflater.Inflate(Resource.Layout.CoinList, null);
view.FindViewById<TextView>(Resource.Id.CoinName).Text = item.Name;
view.FindViewById<ImageView>(Resource.Id.imageView1).SetImageResource(Resource.Drawable.n);
If item is clicked set it on
{
view.FindViewById<Switch>(Resource.Id.switch).SetOn
}
else
{
view.FindViewById<Switch>(Resource.Id.switch).SetOf
}
return view;
}
}
public class TableList
{
public string Name;
public TableList(string Name)
{
this.Name = Name;
}
}
I don't know where I should set the Switch ON (in the listView.ItemClick event or in the ListAdapter) and I don't know how to set it to ON. Please help me to do so.
Here is my demo.
You can choose one to achieve your goal. I will show you how to do this by ItemClick event:
When a Listview item is clicked, I want to turn the row's item switch on.
Because, Switch will grab the focus from ViewGroup. So, I remove the focus from Switch in the MyAdapter:
holder.ms.Focusable = false;//ms is Switch
Now, this is my ItemClick event( turn switch on while click the item):
private void MListView_ItemClick(object sender, AdapterView.ItemClickEventArgs e)
{
var ll = e.View as LinearLayout;
var sw = ll.GetChildAt(1) as Switch;
if (sw.Checked)
{
sw.Checked = false;
adapter.changeState((int)sw.Tag,false);
}
else
{
sw.Checked = true;
adapter.changeState((int)sw.Tag, true);
}
}
As we all know, ListView has reuse problem, so, I add a bool property to control the Switch's state:
public class MyData:Java.Lang.Object {
public MyData(string p,bool b) {
this.position = p;
this.isCheck = b;
}
public string position { get; set; }
public bool isCheck { get; set; }
}
Below is changeState method:
internal void changeState(int position, bool v)
{
mitems[position].isCheck = v;
this.NotifyDataSetChanged();
}
And this is CheckedChange event:
private void Ms_CheckedChange(object sender, CompoundButton.CheckedChangeEventArgs e)
{
var sm = sender as Switch;
Log.Error("Ms_CheckedChange", (int)sm.Tag+"");
if (e.IsChecked&&!mitems[(int)sm.Tag].isCheck)
{
mitems[(int)sm.Tag].isCheck = true;
this.NotifyDataSetChanged();
}
else if(!e.IsChecked&& mitems[(int)sm.Tag].isCheck)
{
mitems[(int)sm.Tag].isCheck = false;
this.NotifyDataSetChanged();
}
}

How do I use MVVM and Xamarin.Forms ListView ItemSelected event to to retrieve a bound property of the item selected?

I am using Xamarin.Forms and attempting to use the MVVM architecture.
I have a ContentPage that has a simple List View on it. The List View has its item source property bound to my ViewModel. I am able to populate the list just fine. All of the items display as they should.
When I click on an item from the list, I need to navigate to a different page based on the item selected. This is what is not working. It will ONLY work if I reference my underlying Model directly (which is NOT what I want to do)
I have the ListView.ItemSelected event coded. However, the Item Selected event can't determine what the "display_text" is of the List Item selected. How do I achieve this without having to reference my model directly from my View (Page)?
MainPage Code:
public partial class MainPage : ContentPage
{
private int intPreJobFormID = 0;
public MainPage()
{
InitializeComponent();
BindingContext = new MainPageViewModel();
Label lblHeader = new Label
{
Text = "HW Job Assessments",
FontSize = ViewGlobals.lblHeader_FontSize,
HorizontalOptions = ViewGlobals.lblHeader_HorizontalOptions,
FontAttributes = ViewGlobals.lblHeader_FontAttributes,
TextColor = ViewGlobals.lblHeader_TextColor
};
//Create the Main Menu Items List View
var lvMain = new ListView
{
//Pull down to refresh list
IsPullToRefreshEnabled = true,
//Define template for displaying each item.
//Argument of DataTemplate constructor is called for each item. It must return a Cell derivative.
ItemTemplate = new DataTemplate(() =>
{
//Create views with bindings for displaying each property.
Label lblDisplayText = new Label();
lblDisplayText.SetBinding(Label.TextProperty, "display_text");
lblDisplayText.FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label));
//Return an assembled ViewCell.
return new ViewCell
{
View = new StackLayout
{
Padding = new Thickness(20, 5, 0, 0),
Orientation = StackOrientation.Horizontal,
Children =
{
new StackLayout
{
VerticalOptions = LayoutOptions.Center,
Spacing = 0,
Children =
{
lblDisplayText
}
}
}
}
};
})
};
lvMain.SetBinding(ListView.ItemsSourceProperty, "MainMenuItems");
lvMain.ItemSelected += lvMain_ItemSelected;
}
private async void lvMain_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var lv = (ListView)sender;
if (e.SelectedItem == null)
{
return; //ItemSelected is called on deselection which results in SelectedItem being set to null
}
//var item = e.SelectedItem as TableMainMenuItems; //This is what I DON'T want to use because it references my Model directly.
var item = e.SelectedItem;
switch (item.display_text) //This is what I need. I can't get this unless I reference my Model "TableMainMenuItems" directly.
{
case "Forms List":
await Navigation.PushAsync(new FormsListPage());
break;
case "New Pre-Job":
await Navigation.PushAsync(new PreJobPage(intPreJobFormID));
break;
}
//Comment out if you want to keep selections
lv.SelectedItem = null;
}
}
MainPageViewModel Code:
public class MainPageViewModel
{
public int intPreJobFormID = 0;
private DatabaseApp app_database = ViewModelGlobals.AppDB;
private DatabaseFormData formdata_database = ViewModelGlobals.FormDataDB;
private IEnumerable<TableMainMenuItems> lstMaineMenuItems;
private IEnumerable<TableFormData> lstRecentJobs;
public string DisplayText { get; set; }
public IEnumerable<TableMainMenuItems> MainMenuItems
{
get { return lstMaineMenuItems; }
set
{
lstMaineMenuItems = value;
}
}
public IEnumerable<TableFormData> RecentJobs
{
get { return lstRecentJobs; }
set
{
lstRecentJobs = value;
}
}
public MainPageViewModel()
{
intPreJobFormID = app_database.GetForm(0, "Pre-Job Assessment").Id;
MainMenuItems = app_database.GetMainMenuItems();
RecentJobs = formdata_database.GetFormAnswersForForm(intPreJobFormID).OrderByDescending(o => o.date_modified);
}
}
There are 2 ways to retrieve the bound property of the item selected.
I personally prefer to handle the logic in the view because it keeps the code simpler.
1. Handle Logic In The View
cast item as your model type:
var item = e.SelectedItem as TableMainMenuItems;
async void lvMain_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var listView = (ListView)sender;
listView.SelectedItem = null;
if (e?.SelectedItem is TableMainMenuItems item)
{
switch (item.display_text)
{
case "Forms List":
await Navigation.PushAsync(new FormsListPage());
break;
case "New Pre-Job":
await Navigation.PushAsync(new PreJobPage(intPreJobFormID));
break;
}
}
}
2. Handle Logic In The View Model
View
Use a Command<T> to loosely couple the ItemSelected logic between the View and the View Model. Create an event in the View Model that will fire when the View Model has completed the logic.
public partial class MainPage : ContentPage
{
public MainPage()
{
var viewModel = new MainPageViewModel();
BindingContext = viewModel;
...
viewModel.NavigationRequested += (s,e) => Device.BeginInvokeOnMainThread(async () => await Navigation.PushAsync(e));
}
...
void lvMain_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var listView = (ListView)sender;
listView.SelectedItem = null;
var viewModel = (MainPageViewModel)BindingContext;
viewModel.ListViewItemSelectedCommand?.Invoke(e);
}
}
ViewModel
public class MainPageViewModel
{
//...
Command<SelectedItemChangedEventArgs> _listViewItemSelectedCommand;
//...
public event EventHandler<Page> NavigationRequested;
//...
public Command<SelectedItemChangedEventArgs> ListViewItemSelectedCommand => _listViewItemSelectedCommand ??
(_listViewItemSelectedCommand = new Command<SelectedItemChangedEventArgs>(ExecuteListViewItemSelectedCommand));
//...
void ExecuteListViewItemSelectedCommand(SelectedItemChangedEventArgs e)
{
var item = e as TableMainMenuItems;
switch (item?.display_text)
{
case "Forms List":
OnNavigationRequested(new FormsListPage());
break;
case "New Pre-Job":
OnNavigationRequested(new PreJobPage(0));
break;
}
}
void OnNavigationRequested(Page pageToNavigate) => NavigationRequested?.Invoke(this, pageToNavigate);
//...
}

ListView with BaseAdapter, show only one row

I created a class called AddItemBaseAdapter that adds a new row in the ListView. The problem is that when adding a new row, the previous row is deleted. To add a new row I have a editTex and a button on Main.axml. For this case it is better to use ArrayAdapter or BaseAdapter continue using?
AddItemBaseAdapter.cs:
public class AddItemBaseAdapter: BaseAdapter<string>
{
string textReceivedEditText;
Activity context;
public AddItemBaseAdapter(Activity context, string textReceivedEditText) : base()
{
this.context = context;
this.textReceivedEditText = textReceivedEditText;
}
public override long GetItemId(int position){
return position;
}
public override string this[int position] {
get { return textReceivedEditText; }
}
public override int Count {
get { return 1; }
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
View view = convertView; // re-use an existing view, if one is available
if (view == null)
view = context.LayoutInflater.Inflate (Resource.Layout.newText, null);
view.FindViewById<TextView>(Resource.Id.singleText).Text = textReceivedEditText;
return view;
}
}
OnClick in MainActivity.cs
btnAddNewRow.Click += delegate(object sender, EventArgs e) {
string textFromEditText = FindViewById<EditText> (Resource.Id.editText).Text;
if(!(textFromEditText.Equals(string.Empty)))
{
_HistoryList = FindViewById<ListView>(Resource.Id.TextHistoryList);
_HistoryList.Adapter = new AddItemBaseAdapter(this, textFromEditText);
}
FindViewById<EditText> (Resource.Id.editText).Text = string.Empty;
};
You are creating an entirely new AddItemBaseAdapter every time the button is clicked.
_HistoryList.Adapter = new AddItemBaseAdapter(this, textFromEditText);
Your adapter does not even have the capacity to store more than one item as it stands now. Perhaps try using (or at least studying) the ArrayAdapter class to start. Then if you require futher functionality beyond that, you can extend it or write your own adapter.

Manipulating collection from the inside

I have a collection of panels which are highlighted when user clicks on them. I want to force them to behave as a set of radio buttons so only the one that is clicked on is highlighted and others aren't.
I guess that there must be a way to manipulate whole collection (set property to false) from the inside, because the event is triggered by one item from the collection. Is there a way for the one item to manipulate whole collection? This is such a common feature in applications so I guess there must be a pattern how to do it properly. Thanks.
You may store collection of your panels and handle required functionality as in following code snippet:
List<Panel> Panels;
private void Initialization()
{
Panels = new List<Panel>();
Panels.Add(pnl1);
Panels.Add(pnl2);
//add all your panels into collection
foreach(Panel Item in this.Panels)
{
//add handle to panel on click event
Item.Click += OnPanelClick;
}
}
private void OnPanelClick(object sender, EventArgs e)
{
foreach(Panel Item in this.Panels)
{
//remove highlight from your panels, real property should have other name than Panel.HighlightEnabled
Item.HighlightEnabled = false;
}
((Panel)sender).HighlightEnabled = true; //add highlight to Panel which invoked Click event
Application.DoEvents(); //ensure that graphics redraw is completed immediately
}
private void AddNewPanelIntoLocalCollection(Panel panel)
{
//here you can add new items to collection during program lifecycle
panel.Click += OnPanelClick;
this.Panels.Add(panel);
}
This is how I do it
public class SelectOne : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private bool isSelected = false;
private HashSet<SelectOne> selecteOnes = null;
public bool IsSelected
{
get { return isSelected; }
set
{
if (isSelected == value) return;
if (isSelected && selecteOnes != null)
{
foreach (SelectOne so in selecteOnes)
{
if (so == this) continue;
so.IsSelected = false;
}
}
NotifyPropertyChanged("IsSelected");
}
}
public SelectOne() { }
public SelectOne(bool IsSelected) { isSelected = IsSelected; }
public SelectedOne(bool IsSelected, HashSet<SelectOne> SelecteOnes)
{
isSelected = IsSelected;
selecteOnes = SelecteOnes;
}
}
Eventually I did find a way to do this properly with only one delegate.
In class A I have a collection of objects B
List<B> b = new List<B>
class B, needs to have an unique ID and delegete for void metod with Id parameter
delegate void DeleteItemDelegate(int id);
class B
{
public int ID {get; set;}
public DeleteItemDeleate deleteThis {set; get;}
}
class A has a metod like this:
public void RemoveItem(int id)
{
for (int x = 0; x < b.Count; x++)
{
if (b[x].id == id)
{
b.RemoveAt(x);
}
}
}
when adding a new B object into List just add metod RemoveItem to B.deleteThis delegate
B bObject = new B();
bObject.deleteThis = RemoveItem;
b.Add(bObject);
Now all you need to do is add DeleteMe metod in B class
void DeleteMe()
{
// and call local delegate - pointing to metod which actually can manipulate the collection
deleteThis(id);
}

Categories