CollectionView using DataTemplate is not updating ReactiveContentView BindingContext or ViewModel - xamarin.forms

I'm using ReactiveUI and trying to get a CollectionView to use a ReactiveContentPage as it's item template in code behind. This works okay until the ReactiveContentView is nested under another Element.
The problem I have is that after scrolling the collection view, the items get duplicated. After debugging, it would seem that the ReactiveContentView's BindingContext or ViewModel aren't updated.
The samples below have been simplified to highlight the issue:
This doesn't work:
public partial class SampleCollectionPage : ReactiveContentPage<SampleCollectionViewModel>
{
private void Build()
{
Content = new CollectionView
{
ItemTemplate = new DataTemplate(() =>
{
return new ContentView
{
Content = new ReactiveContentView<CollectionItemViewModel>
{
Content = new StackLayout
{
Children =
{
new Label()
.Bind(Label.TextProperty, nameof(CollectionItemViewModel.Title)),
new Label()
.Bind(Label.TextProperty, nameof(CollectionItemViewModel.Description)),
new Label()
.Bind(Label.TextProperty, nameof(CollectionItemViewModel.SomeTime)),
}
}
}
};
}),
}
.Bind(CollectionView.ItemsSourceProperty, path: nameof(SampleCollectionViewModel.Items));
}
}
This works:
public partial class SampleCollectionPage : ReactiveContentPage<SampleCollectionViewModel>
{
private void Build()
{
Content = new CollectionView
{
ItemTemplate = new DataTemplate(() =>
{
return new ReactiveContentView<CollectionItemViewModel>
{
Content = new StackLayout
{
Children =
{
new Label()
.Bind(Label.TextProperty, nameof(CollectionItemViewModel.Title)),
new Label()
.Bind(Label.TextProperty, nameof(CollectionItemViewModel.Description)),
new Label()
.Bind(Label.TextProperty, nameof(CollectionItemViewModel.SomeTime)),
}
}
};
}),
}
.Bind(CollectionView.ItemsSourceProperty, path: nameof(SampleCollectionViewModel.Items));
}
}
The only difference being that the working version doesn't put the ReactiveContentView in a ContentView. I'm not sure if I'm missing something or whether this is an bug in Xamarin Forms or ReactiveUI?
(edited to add actual problem: doh)

Related

How to assign the grid to a grid

I new a grid object without any settings in the xaml file.
After importing the data through 'Build_Grid()' in the 'OnAppearing()', I want to assign the 'gridview' to 'Grid_Info' to display on the screen, the code 'Grid_Info = gridview' does not work.
I am wondering how to achieve my needs?
<ContentPage.Content>
<StackLayout>
<Label Text="AAA" />
<Grid x:Name="Grid_Info">
</Grid>
</StackLayout>
</ContentPage.Content>
void Build_Grid(Data data)
{
Grid gridview = new Grid();
gridview.RowDefinitions.Add(new RowDefinition() { Height = 40 });
gridview.Children.Add(data[0],0,0);
...
Grid_Info = gridview; //it does not work...
}
protected override void OnAppearing()
{
Data data = new Data();
...
Build_Grid(data);
}
if you've already defined the Grid in XAML there is no need to do this
Grid gridview = new Grid();
instead just reference Grid_Info directly
Grid_Info.RowDefinitions.Add(new RowDefinition() { Height = 40 });
Grid_Info.Children.Add(data[0],0,0);
You could try the code below. I make a Data class with view to test. It work for me.
public partial class Page3 : ContentPage
{
Data[] data;
public Page3()
{
InitializeComponent();
data = new Data[2];
data[0] = new Data { view = new Button() { BackgroundColor = Color.Red } }; //data[0]
data[1] = new Data { view = new Label() { BackgroundColor = Color.Green, Text = "Label" } };//data[1]
}
void Build_Grid(Data[] data)
{
Grid gridview = new Grid();
Grid_Info.RowDefinitions.Add(new RowDefinition() { Height = 40 });
Grid_Info.Children.Add(data[0].view, 0, 0);//show the red button
Grid_Info = gridview; //it does not work...
}
protected override void OnAppearing()
{
base.OnAppearing();
Build_Grid(data);
}
}
public class Data
{
public View view { get; set; }
}

How to bind Layout to ViewModel from code?

I'm trying to bind items to a StackLayout via this documentation:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/layouts/bindable-layouts
I don't use XAML, so I obviously need to bind from inside my code, the documentation shows how to do it this way:
IEnumerable<string> items = ...;
var stack = new StackLayout();
BindableLayout.SetItemsSource(stack, items);
But I need to reference a property from withing my ViewModel, set earlier via the View's BindingContext. Could someone please help me doing this?
Something ala (pseudocode):
var stack = new StackLayout() { ... };
stack.SetBinding(StackLayout.ItemsSource, "Items")
I dont want my controller to know anything about the actual viewModel, and the way its suggested, I need to use it in a typed matter, where I should know the ViewModel.
Below is an example of what I'm trying to accomplish. Please note I use NO XAML at all! Write all my UI in code:
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace LayoutTest
{
public class MyPage : ContentPage
{
public MyPage()
{
BindingContext = new MyViewModel();
var layout = new StackLayout();
BindableLayout.SetItemsSource(layout, "?????");
BindableLayout.SetItemTemplate(layout, new DataTemplate(() =>
{
var lbl = new Label();
lbl.SetBinding(Label.TextProperty, "Name");
return lbl;
}));
Content = layout;
}
}
public class MyViewModel
{
List<Item> Items { get; set; }
public MyViewModel()
{
Items = new List<Item>();
Items.Add(new Item { Name = "Kent" });
Items.Add(new Item { Name = "Tony" });
Items.Add(new Item { Name = "Allan" });
}
}
public class Item
{
public string Name { get; set; }
}
}
Dont know what to write in this line:
BindableLayout.SetItemsSource(layout, "?????");
It only take a collection as a property, but then I need to know about the ViewModel, but I dont want that.
What to do?
layout.SetBinding(BindableLayout.ItemsSourceProperty, new Binding("Items”));
Do that instead of the BindableLayout.SetItemsSource that you have now. The binding will use the existing binding context when setting this.
#KenFonager from code behind you could do this as simple as that.
BindingContext = new ViewModels.MainPages.LiveStreamViewModel();
closeButton.SetBinding(Button.CommandProperty, "BackCommand");

Context menu on TableRow<Object> does not show up on first right click

So I followed this example on using context menu with TableViews from here. I noticed that using this code
row.contextMenuProperty().bind(Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu)null));
does not show up on first right click on a row with values. I need to right click on that row again for the context menu to show up. I also tried this code(which is my first approach, but not using it anymore because I've read somewhere that that guide is the best/good practice for anything related about context menu and tableview), and it displays the context menu immediately
if (row.getItem() != null) {
rowMenu.show(row, event.getScreenX(), event.getScreenY());
}
else {
// do nothing
}
but my problem with this code is it throws a NullPointerException whenever i try to right click on a row that has no data.
What could I possibly do to prevent NullPointerException while having the context menu show up immediately after a right click? In my code, I also have a code that a certain menu item in the context menu will be disabled based on the property of the myObject binded to row, that's why i need the context menu to pop up right away.
I noticed this too with the first block of code. Even if the property of myObject has already changed, it still has a menu item enabled/disabled unless I right click on that row again. I hope that you could help me. Thank you!
Here is a MCVE:
public class MCVE_TableView extends Application{
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane myBorderPane = new BorderPane();
TableView<People> myTable = new TableView<>();
TableColumn<People, String> nameColumn = new TableColumn<>();
TableColumn<People, Integer> ageColumn = new TableColumn<>();
ContextMenu rowMenu = new ContextMenu();
ObservableList<People> peopleList = FXCollections.observableArrayList();
peopleList.add(new People("John Doe", 23));
nameColumn.setMinWidth(100);
nameColumn.setCellValueFactory(
new PropertyValueFactory<>("Name"));
ageColumn.setMinWidth(100);
ageColumn.setCellValueFactory(
new PropertyValueFactory<>("Age"));
myTable.setItems(peopleList);
myTable.getColumns().addAll(nameColumn, ageColumn);
myTable.setRowFactory(tv -> {
TableRow<People> row = new TableRow<>();
row.setOnContextMenuRequested((event) -> {
People selectedRow = row.getItem();
rowMenu.getItems().clear();
MenuItem sampleMenuItem = new MenuItem("Sample Button");
if (selectedRow != null) {
if (selectedRow.getAge() > 100) {
sampleMenuItem.setDisable(true);
}
rowMenu.getItems().add(sampleMenuItem);
}
else {
event.consume();
}
/*if (row.getItem() != null) { // this block comment displays the context menu instantly
rowMenu.show(row, event.getScreenX(), event.getScreenY());
}
else {
// do nothing
}*/
// this requires the row to be right clicked 2 times before displaying the context menu
row.contextMenuProperty().bind(Bindings.when(Bindings.isNotNull(row.itemProperty()))
.then(rowMenu)
.otherwise((ContextMenu)null));
});
return row;
});
myBorderPane.setCenter(myTable);
Scene scene = new Scene(myBorderPane, 500, 500);
primaryStage.setTitle("MCVE");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main (String[] args) {
launch(args);
}
}
Here is the People Class
public class People {
SimpleStringProperty name;
SimpleIntegerProperty age;
public People(String name, int age) {
this.name = new SimpleStringProperty(name);
this.age = new SimpleIntegerProperty(age);
}
public SimpleStringProperty NameProperty() {
return this.name;
}
public SimpleIntegerProperty AgeProperty() {
return this.age;
}
public String getName() {
return this.name.get();
}
public int getAge() {
return this.age.get();
}
}
Edit: MCVE added
Edit2: Updated the MCVE. Still requires to be right-clicked twice before the contextMenu pops up
Below's a code snippet as a quick demonstration of how-to/where-to instantiate and configure a per-row ContextMenu. It
creates a ContextMenu/MenuItem for each TableRow at the row's instantiation time
creates a conditional binding that binds the menu to the row's contextMenuProperty if not empty (just the same as you did)
configures the contextMenu in an onShowing handler, depending on the current item (note: no need for a guard against null, because the conditional binding will implicitly guarantee to not show the the menu in that case)
The snippet:
myTable.setRowFactory(tv -> {
TableRow<People> row = new TableRow<>() {
ContextMenu rowMenu = new ContextMenu();
MenuItem sampleMenuItem = new MenuItem("Sample Button");
{
rowMenu.getItems().addAll(sampleMenuItem);
contextMenuProperty()
.bind(Bindings
.when(Bindings.isNotNull(itemProperty()))
.then(rowMenu).otherwise((ContextMenu) null));
rowMenu.setOnShowing(e -> {
People selectedRow = getItem();
sampleMenuItem.setDisable(selectedRow.getAge() > 100);
});
}
};
return row;
});

Xamarin forms MasterDetailPage navigation

So ive been working on an app that uses a MasterDetail page and its going fine but Im just a little bit confused on how its suppose to navigate through pages.
At the moment i have the menu items opening some pages in the app and that parts working great, the side menu stays. The thing im confused with is how to handle having buttons on the main page being displayed. My buttons at the moment just open up a new page but the side menu of the MasterDetail page just disappears into the regular NavigationPage.
I will give my button code below.
btnSocial.GestureRecognizers.Add(new TapGestureRecognizer
{
Command = new Command(() =>
{
Navigation.PushAsync(new SocialPage());
})
});
Is this just how a MasterDetail page navigates or do you think im doing something wrong?
** EDITED **
Just incase this helps, i will attach my menuopage and launchpage code:
MenuPage.cs
public class MenuPage : ContentPage
{
public Action<ContentPage> OnMenuSelect { get; set; }
public MenuPage()
{
Title = "Menu";
Icon = "ic_menu.png";
BackgroundColor = ProjectVariables.PRIMARY_COLOR;
var items = new List<MenuItems>()
{
new MenuItems("Social", () => new SocialPage()),
new MenuItems("Career", () => null),
new MenuItems("MySchedule", () => null),
new MenuItems("Videos", () => null),
new MenuItems("Contact", () => null),
new MenuItems("Sign in", () => null)
};
var dataTemplate = new DataTemplate(typeof(TextCell));
dataTemplate.SetValue(TextCell.TextColorProperty, Color.White);
dataTemplate.SetBinding(TextCell.TextProperty, "Name");
var listview = new ListView()
{
ItemsSource = items,
ItemTemplate = dataTemplate
};
listview.BackgroundColor = ProjectVariables.PRIMARY_COLOR;
listview.ItemSelected += (object sender, SelectedItemChangedEventArgs e) =>
{
if(OnMenuSelect != null)
{
var item = (MenuItems)e.SelectedItem;
var itemPage = item.PageFn();
OnMenuSelect(itemPage);
}
};
Content = new StackLayout
{
Orientation = StackOrientation.Vertical,
Children =
{
listview
}
};
}
}
LaunchPage.cs
public class LaunchPage : MasterDetailPage
{
public LaunchPage()
{
var menuPage = new MenuPage();
menuPage.OnMenuSelect = (categoryPage) =>
{
Detail = new NavigationPage(categoryPage);
//Detail.Navigation.PushAsync(categoryPage);
IsPresented = false;
};
Master = menuPage;
Detail = new NavigationPage(new MainPage())
{
BarTextColor = Color.White,
BarBackgroundColor = ProjectVariables.PRIMARY_COLOR
};
MasterBehavior = MasterBehavior.Split;
}
}
Have a look at this documentation page from Xamarin.
It looks like you do not use the navigation service for this. You need a reference to your master page and set the Detail property for it.
Look at this section in particular.
public partial class MainPage : MasterDetailPage
{
public MainPage ()
{
...
masterPage.ListView.ItemSelected += OnItemSelected;
}
void OnItemSelected (object sender, SelectedItemChangedEventArgs e)
{
var item = e.SelectedItem as MasterPageItem;
if (item != null) {
Detail = new NavigationPage ((Page)Activator.CreateInstance (item.TargetType));
masterPage.ListView.SelectedItem = null;
IsPresented = false;
}
}
}
On the selection of a ListView item they set the Detail property and it will do the navigation for you.

Xamarin Forms - How To Use A ListVIew

I am trying to populate a listview with a customcell I made. I know how to do it with a tableview, but i have no idea how to do it with a listview. Can someone explain to me please, thank you
Excuse the very rough snippet of code but it is merely to explain the concept behind listviews and databinding.
In my example below I create a listview, assign a custom viewCell and bind the view cell to a model. The model data is populated by XML data retrieved from the server(Code emitted for simplicity). Simpler implementations can be done should you require a listview with hardcoded data.
I suggest reading through the http://developer.xamarin.com/guides/cross-platform/xamarin-forms/introduction-to-xamarin-forms/ and researching the MVVM principle
Additionally the best example code I have found to go through is an enlightening application written by James Montemagno https://github.com/jamesmontemagno/Hanselman.Forms
public TestPage()
{
private ListView listView; // Create a private property with the Type of Listview named listview
listView = new ListView(); //Instantiate the listview
var viewTemplate = new DataTemplate(typeof(CustomViewCell)); //Create a variable for the custom data template
listView.ItemTemplate = viewTemplate; // set the data template to the variable created above
this.Content = new StackLayout()
{
Children = {
listView
}
};
}
Custom View Cell:
public class CustomViewCell : ViewCell
{
public CustomViewCell()
{
Label Test = new Label()
{
Font = Font.BoldSystemFontOfSize(17),
TextColor = Helpers.Color.AppGreen.ToFormsColor(),
BackgroundColor = Color.White
};
var image = new Image();
image.Source = ImageSource.FromFile("testing.png");
Label Test2 = new Label();
tour_Desc.SetBinding(Label.TextProperty, "Test");
round_Desc.SetBinding(Label.TextProperty, "Test2");
var grid = new Grid
{
VerticalOptions = LayoutOptions.FillAndExpand,
RowDefinitions =
{
new RowDefinition {Height = GridLength.Auto },
new RowDefinition {Height = GridLength.Auto },
new RowDefinition {Height = 1}
},
ColumnDefinitions =
{
new ColumnDefinition {Width = GridLength.Auto},
new ColumnDefinition {Width = new GridLength(1, GridUnitType.Star)},
new ColumnDefinition {Width = 80 }
}
,BackgroundColor = Color.White,
};
grid.Children.Add(image, 0, 0);
Grid.SetRowSpan(image, 2);
grid.Children.Add(tour_Desc, 1, 0);
Grid.SetColumnSpan(tour_Desc, 2);
grid.Children.Add(Test, 1, 1);
grid.Children.Add(Test2, 1, 2);
this.View = grid;
}
}
View Model:
public partial class GamesResult
{
public string Test { get; set; }
public string Test1 { get; set; }
}

Resources