How to bind a ReactiveCommand to a control in a Xamarin.Forms ListView? - xaml

I am using ReactiveUI, Xamarin.Forms and XAML. I am trying to implement a simple scenario with a ListView where each row has a delete button. Here is the ListView XAML:
<ListView x:Name="playerListView" ItemsSource="{Binding Players}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal" Padding="20, 5, 20, 5">
<Label Text="{Binding .}" VerticalOptions="CenterAndExpand" HorizontalOptions="StartAndExpand" />
<Button x:Name="deleteButton" Text="Delete" Clicked="onDeleteClicked" VerticalOptions="CenterAndExpand" HorizontalOptions="EndAndExpand" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
As you can see, the delete button has a Clicked handler registered. This works but it does not feel like the RxUI way. Here is the code behind:
private void onDeleteClicked(object sender, EventArgs e)
{
var button = (Button)sender;
this.ViewModel.RemovePlayer.Execute(button.BindingContext);
}
How can I replace this onDeleteClicked event handler with a declarative binding to my RemovePlayer command? I can't see a good way to do it because I chose to bind the ListView to a ReactiveList<string>, so if I try to do Command="{Binding RemovePlayer}" it fails because the cell is bound to a string.
For completeness here is my view model:
public class NewGameViewModel : ReactiveObject
{
public ReactiveList<string> Players { get; private set; }
public ReactiveCommand<Object> AddPlayer { get; private set; }
public ReactiveCommand<Object> RemovePlayer { get; private set; }
public ReactiveCommand<Object> StartGame { get; private set; }
public ReactiveCommand<Object> RandomizeOrder { get; private set; }
string newPlayerName;
public string NewPlayerName {
get { return newPlayerName; }
set { this.RaiseAndSetIfChanged(ref newPlayerName, value); }
}
public NewGameViewModel()
{
Players = new ReactiveList<string> ();
var canStart = this.Players.CountChanged.Select(count => count >= 3);
StartGame = canStart.ToCommand();
RandomizeOrder = canStart.ToCommand();
AddPlayer = this.WhenAnyValue(x => x.Players.Count, x => x.NewPlayerName,
(count, newPlayerName) => count < 7 && !string.IsNullOrWhiteSpace(newPlayerName) && !this.Players.Contains(newPlayerName))
.ToCommand();
RemovePlayer = ReactiveCommand.Create();
}
}

Because there is no relative binding support in Xamarin Forms at the moment (see this Xamarin Forms forums post for more info), you won't be able to bind a command to your Button within your ListViews DataTemplate. Any binding within that DataTemplate will have a BindingContext relative to the current item in the list - in your case, a simple string. If your ListView was bound to an object, let's say a Person, then your Button command binding would still fail with an error something along the lines of No Command RemovePlayer found on object Person
So implementing the Command in the view's code behind like you have done is one option. Another is using a C# DataTemplate (not a XAML one) and implementing the Command there - but both of those are kind of the same thing. Neither are a great solution if you like keeping stuff like that out of your views and only within your view models; but until relative binding support is introduced there aren't really any other options.
I ran into the exact same problem as you, but I was binding my ListView to a collection of objects. The class for my object was in a separate class library that ONLY has POCOs in it, and I did not like the idea of implementing a Command within one of my POCO's.

The tricky bit is that your "SelectedPlayer" isn't exposed in your ViewModel, so there's no way to do this the RxUI way. If it was, you can do something like:
RemovePlayer.Select(_ => SelectedPlayer).Subscribe(x => {
SelectedPlayer = null;
Players.Remove(x);
});
If your Player object was itself a ViewModel and "RemovePlayer" was on the Player itself, you can do this Tricky Trick:
Players.Changed.StartWith(null)
.Select(_ => Players
.Select(x => x.RemovePlayer.Select(__ => x))
.Merge())
.Switch()
.Subscribe(x => Players.Remove(x));
Here, we're saying, "Every time the Players list changes, I want to build a new Observable: Take the list of all the current players, and Select them into an Observable that fires when someone hits a RemovePlayer button - tell me when any of those new Observables fire"

My opinion on this is don't be a purist :)
Is not the best design model, but extend your object to handle a ICommand and bind to it.. it's the best solution for now imo.
If your objects are on the same assembly you can use partial's , if are different , you can create small viewmodel for your poco .

Related

Get Element Root's ViewModel Context in WINUI3

I have a page which contains a ListView x:bound to an object in my ViewModel. This object contains a list of objects (timestamps) that contains a list of Subjects that contains a list of another objects. I'm presenting the data in 2 list views, one inside another.
<ListView
x:Name="primaryList" // for exemplification purposes
ItemsSource="{x:Bind ViewModel.VideoProject.TimeStamps, Mode=OneWay}"
ItemClick='{x:Bind ViewModel.ListViewTimeStamps_ItemClick, Mode=OneWay}'>
The ListView contains a DataTemplate for another ListView
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Spacing="5">
<TextBlock Text="{Binding Id}"
FontSize="15"
HorizontalAlignment="Left"
FontWeight="Bold" />
<ListView ItemsSource="{Binding Subjects}"
x:Name="secondaryList"
SelectionMode="Multiple">
<ListView.ItemTemplate>
....
And the second ListView is followed by another same structure.
My goal is to bind the second ListView ItemClickEvent to ListViewTimeStamps_ItemClick method inside my ViewModel, because I need the data contained in the object that secondaryListView holds (Subject).
I could try to set the Data Template Context to the ViewModel but it would break the Subject bind.
I found a ton of questions about this topic but differently from WPF there's not AncestorType to catch the up tree reference.
Obs:
My project is based on the Template Model which creates the XAML .cs with the ViewModel as a Property. I also haven't set the DataContext on the XAML page because I can x:bind normally my view model to the page elements without explicit set.
Is there a way to accomplish without using Attached Properties?
Thank you.
Since there is no support for setting the AncestorType property of a RelativeSource in WinUI, there is no way to accomplish this in pure XAML without writing some code.
You could implement an attached bevaiour as suggested and exemplified here:
public static class AncestorSource
{
public static readonly DependencyProperty AncestorTypeProperty =
DependencyProperty.RegisterAttached(
"AncestorType",
typeof(Type),
typeof(AncestorSource),
new PropertyMetadata(default(Type), OnAncestorTypeChanged)
);
public static void SetAncestorType(FrameworkElement element, Type value) =>
element.SetValue(AncestorTypeProperty, value);
public static Type GetAncestorType(FrameworkElement element) =>
(Type)element.GetValue(AncestorTypeProperty);
private static void OnAncestorTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FrameworkElement target = (FrameworkElement)d;
if (target.IsLoaded)
SetDataContext(target);
else
target.Loaded += OnTargetLoaded;
}
private static void OnTargetLoaded(object sender, RoutedEventArgs e)
{
FrameworkElement target = (FrameworkElement)sender;
target.Loaded -= OnTargetLoaded;
SetDataContext(target);
}
private static void SetDataContext(FrameworkElement target)
{
Type ancestorType = GetAncestorType(target);
if (ancestorType != null)
target.DataContext = FindParent(target, ancestorType);
}
private static object FindParent(DependencyObject dependencyObject, Type ancestorType)
{
DependencyObject parent = VisualTreeHelper.GetParent(dependencyObject);
if (parent == null)
return null;
if (ancestorType.IsAssignableFrom(parent.GetType()))
return parent;
return FindParent(parent, ancestorType);
}
}
So no replacement so far for AncestorType?
No. Not in XAML.

Why I get exception when setting ComboBox.SelectedIndex?

I am trying to learn about bindings and XAML. I have a very simple example, where I bind an array of strings to a combobox defined in XAML. I also want to set the selected index.
I am getting an exception:
Value does not fall within the expected range.
with the SelectedIndex property.
Here my XAML for a UWP application.
<StackPanel Background="{ThemeResource applicationPageBackgroundThemeBrush}">
<ComboBox Name="BrowserCombo" ItemsSource="{x:Bind ComboStrings}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
and the code behind
public sealed partial class MainPage : Page
{
private string[] comboStrings = { "One", "Two", "Three" };
public List<String> ComboStrings
{
get { return new List<string>(comboStrings); }
}
public MainPage()
{
this.InitializeComponent();
DataContext = this;
BrowserCombo.SelectedIndex = 1;
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
// BrowserCombo.SelectedIndex = 1;
}
}
I want to have it very simple, so no MVVM - I still need to learn this concept.
Remarks:
I can put SelectedIndex in the XAML file. Same problem.
If I put the SelectedIndex in the Page_Loaded event handler, it works fine.
In a classic desktop XAML (aka WPF) there will be no problem.
It seems to be that the item list will be populated differently between XAML-WinRt and XAML-WPF. What is the earliest point to access the SelectedIndex property?
You get this exception because your ItemsSource is still null when you try to set BrowserCombo.SelectedIndex = 1; in the page's constructor. You are using x:Bind - if you debug the generated MainPage.g.cs file and put some breakpoints there:
public void Loading(global::Windows.UI.Xaml.FrameworkElement src, object data)
{
this.Initialize();
}
public void Initialize()
{
if (!this.initialized)
{
this.Update();
}
}
public void Update()
{
this.Update_(this.dataRoot, NOT_PHASED);
this.initialized = true;
}
You will see that x:Bind is being resolved in Loading event - this explains why in constructor you still have null in ItemsSource.
The other thing is that with old Binding - it is resolved when you set DataContext = this;, but you are using x:Bind and in fact you don't need to set the DataContext - it doesn't change anything here. If you replace ItemsSource="{x:Bind ComboStrings}" with ItemsSource="{Binding ComboStrings}" then your code should work, otherwise you may remove the line DataContext = this;.
Another interesting thing is that the order of defined bindings in XAML, can also cause similar troubles - for example if you define your ComboBox like this:
<ComboBox Name="BrowserCombo" SelectedIndex="{x:Bind Index}" ItemsSource="{x:Bind ComboStrings}">
then you will also get exception - SelectedIndex is being resolved before the collection is set up. Opposite to the situation when it works fine:
<ComboBox Name="BrowserCombo" ItemsSource="{x:Bind ComboStrings}" SelectedIndex="{x:Bind Index}">

Property can't be found on ViewModel in UWP app

An Order form in UWP using Template 10 adds products to an order. The error is
Invalid binding path 'OrderViewModel.FindProduct_TextChanged' : Property 'OrderViewModel' can't be found on type 'ProductViewModel'
The relevant xaml snippet is
<Page.DataContext>
<ViewModels:MainPageViewModel x:Name="OrderViewModel" />
</Page.DataContext>
<GridView ItemsSource="{x:Bind OrderViewModel.Products, Mode=TwoWay}">
<GridView.ItemTemplate>
<DataTemplate x:DataType="ViewModels:ProductViewModel" >
<AutoSuggestBox
Name="ProductAutoSuggestBox"
TextMemberPath="{x:Bind ItemCode, Mode=TwoWay}"
TextChanged="{x:Bind OrderViewModel.FindProduct_TextChanged}">
</AutoSuggestBox>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
The relevant snippet from the OrderViewModel and the ProductViewModel
namespace ViewModels
{
public class OrderViewModel : ViewModelBase
{
public ObservableCollection<Product> Products { get; set; } = new ObservableCollection<Product>();
public void FindProduct_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{ ... }
}
public class ProductViewModel : ViewModelBase
{
string _ItemCode = default(string);
public string ItemCode { get { return _ItemCode; } set { Set(ref _ItemCode, value); } }
public ProductViewModel()
{
}
}
}
How to I correctly reference FindProduct_TextChanged on the OrderViewModel from the DataTemplate for the GridView which references ProductViewModel?
Voted up to #tao's comment. #Vague, I think you may misunderstand what x:DataType is used for. You can refer to the "DataTemplate and x:DataType" part of Data binding in depth:
When using {x:Bind} in a data template, so that its bindings can be validated (and efficient code generated for them) at compile-time, the DataTemplate needs to declare the type of its data object using x:DataType.
For your scenario, from your code public ObservableCollection<Product> Products { get; set; } = new ObservableCollection<Product>();, the type of your DataTemplate's data object should be your Product class, not your ProductViewModel, and in the meanwhile, your FindProduct_TextChanged event must be find in this Product class, that means your code of FindProduct_TextChanged should be placed in your Product data model.
By the way, I think there is no need to use TwoWay binding for ItemsSource. For this scenario, the binding target is ItemsSource of GridView, the binding source is ObservableCollection<Product> Products, I understand you want to update GridView when your collection is updated, this is work is done with ObservableCollection. Besides, only the binding source here can be changed to notify the binding target, so OneWay binding is enough. But it's not a big problem with your code.
So for your GridView, it should be something like this:
<GridView ItemsSource="{x:Bind OrderViewModel.Products, Mode=OneWay}">
<GridView.ItemTemplate>
<DataTemplate x:DataType="Models:Product" >
<AutoSuggestBox
Name="ProductAutoSuggestBox"
TextMemberPath="{x:Bind ItemCode, Mode=TwoWay}"
TextChanged="{x:Bind FindProduct_TextChanged}">
</AutoSuggestBox>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
if error is kind of like this I approved its a charset support bug:
Error Invalid binding path 'XX.YY' : Property 'ZZ' can't be found on type 'CCC'
Either xaml and C# supports unicode;
its because you use a non-ascii character in class properties. this is a bug I found today. Just rename your class proprty characters to ascii standards. Hope it will be fixed.

Command parameter in EventTrigger

I'm working on WinRT with MvmmCross v3 framework and Windows.UI.Interactivity.
I want a TextBox with an EventTrigger on the event TextChanged which launch a Command. And also, I want in CommandParameter the text of the textBox.
So I have this code
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding UpdateText}" CommandParameter="{Binding Text}"/>
</i:EventTrigger>
public ICommand UpdateText
{
get
{
return new MvxCommand<object>((textSearch) =>
{
// code...
});
}
}
But my textSearch parameter equals to "{Windows.UI.Xaml.Controls.TextChangedEventArgs}" with all of these properties NULL.
I Tried also to declare explicitly my ElementName in the binding like
CommandParameter="{Binding Path=Text, ElementName=tes}
But it failed too.
Thanks
Do you really need to handle TextChanged event? You could be notified of the changes by just binding to the Text property:
<TextBox Text="{Binding TextValue, Mode=TwoWay}" />
And then in the view model:
private string _textValue;
public string TextValue
{
get
{
return _textValue;
}
set
{
if (_textValue == value)
return;
_textValue = value;
OnTextChanged(value); // react to the changed value
}
}
EDIT:
There are two things you need to be aware of, if you want to get to the Text value from inside your Command:
First, you need to fix the CommandParameter binding. By using {Binding Text} you are actually trying to bind to a property in your view model, i.e. you would first need to bind the TextBox.Text property to the same view model property. As you've said in the comment, that's no good for you because you need the info on every change and not only on lost focus, so the value you get is not up to date enough.
The right approach would therefore be your second attempt, i.e. binding directly to the TextBox using the ElementName syntax. Unfortunately triggers are not a part of the visual tree therefore they don't get access to the XAML name scope (you can read more about it in this blog post). You can work around that by using NameScopeBinding from MVVMHelpers.Metro package:
<Behaviors:NameScopeBinding x:Key="MyTextBox"
Source="{Binding ElementName=MyTextBox}" />
Now you can make the ElementName binding work:
<i:InvokeCommandAction Command="{Binding UpdateText}"
CommandParameter="{Binding Source.Text, Source={StaticResource MyTextBox}}"/>
You still have the second problem. The Text value that you are binding to only updates on lost focus so you don't get the right value when handling TextChanged event. The solution is to bind to the TextBox itself:
<i:InvokeCommandAction Command="{Binding UpdateText}"
CommandParameter="{Binding Source, Source={StaticResource MyTextBox}}"/>
And then inside the command get the Text property directly from the TextBox:
private void OnUpdateText(object parameter)
{
var value = ((TextBox) parameter).Text;
// process the Text value as you need to.
}
EDIT 2:
To make the above code work with the view model being in a PCL, there a couple of approaches you could take.
The simplest hack would be to use reflection. Since it is available in PCL you could get to the Text property and read its value:
private void OnUpdateText(object parameter)
{
var textProperty = textSearch.GetType().GetProperty("Text");
var value = textProperty.GetValue(parameter, null) as string;
// process the Text value as you need to.
}
A cleaner solution would be to put the WinRT specific code into a separate assembly abstracted via an interface. You would first create an interface in the PCL library:
public interface IUiService
{
string GetTextBoxText(object textBox);
}
And modify view model to accept this interface in the constructor:
public ViewModel(IUiService uiService)
{
_uiService = uiService;
}
In the command handler you would than use the method in the interface:
private void OnUpdateText(object parameter)
{
var value = _uiService.GetTextBoxText(parameter);
// process the Text value as you need to.
}
You would implement that interface in a WinRT library:
public class UiService : IUiService
{
public string GetTextBoxText(object textBox)
{
var typedTextBox = textBox as TextBox;
return typedTextBox.text;
}
}
In the application you reference this library and pass the implementation to view model:
var viewModel = new ViewModel(new UiService);
My favorite approach is different: I'd create a Behavior exposing a Text property automatically updated every time TextChanged event is triggered:
public class TextChangedBehavior : Behavior<TextBox>
{
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text",
typeof(string),
typeof(TextChangedBehavior),
new PropertyMetadata(null));
public string Text
{
get { return (string) GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.TextChanged += OnTextChanged;
Text = AssociatedObject.Text;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.TextChanged -= OnTextChanged;
}
private void OnTextChanged(object sender, TextChangedEventArgs textChangedEventArgs)
{
Text = AssociatedObject.Text;
}
}
Now I could bind a TextValue property to this behavior and react to its change in the property setter as already suggested at the very beginning of this long answer:
<TextBox>
<i:Interaction.Behaviors>
<b:TextChangedBehavior Text="{Binding TextValue, Mode=TwoWay}" />
</i:Interaction.Behaviors>
</TextBox>

Silverlight: Data Binding: Using of Dependency Properties within a Row of a Collection

I have an collection + its structure:
public class FunctionListBindStructure : AttributeBase
{
public FunctionListBindStructure() : base(true) { }
//this represents one row of the collection
public MyFunction Function { get; set; }
public string Name { get; set; }
}
public class FunctionListBind : AttributeListBase
{
//this represents
public ObservableCollection<FunctionListBindStructure> FunctionList { get; set; }
public FunctionListBind()
: base(true)
{
FunctionList = new ObservableCollection<FunctionListBindStructure>();
}
public override IList GetList()
{
return FunctionList as IList;
}
}
This class makes usage of a framework, which generates a Dependency Property for the CLR property Function.DisplayName as "FunctionDisplayNameProperty".
In my example view I bind this collection to a ListBox
ListBox ItemsSource="{Binding MyModel.FunctionListBind.FunctionList}" Height="52" HorizontalAlignment="Left" Margin="136,157,0,0" Name="listBox1" VerticalAlignment="Top" Width="130" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding FunctionDisplayNameProperty, Mode=TwoWay}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The problem is now that only the last item of the collection is displayed in the list...the previous items are rendered only with space; although I am pretty sure (via debugger) that the dependendy properties (when they registered and their value set) of the previous rows shall have non-initial values. If I refer directly to the corresponding CLR property (Function.DisplayName) all works fine.
My question: Do I make here a design error? Are Dependency Properties not supposed to be used as a row type? I use the same pattern for non-collection and there it works. This is also the reason why I want to use the same approach for collections (I can use 90% of the exisitng codeline to generate and set the Dependeny Properties).
Thanks for any hints also how (if not a design error) to debug the Dependency Property binding.
It was just a bug of my framework coding. I have defined the dependency properties as an instance attribute, instead of a static one. Now all works fine.