Xamarin Forms ListView SelectedItem Binding Issue - c#

ListViews follow the ItemPicker/Selector pattern of UI controls. Generally speaking, these types of controls, regardless of their platform will have a SelectedItem, and an ItemsSource. The basic idea is that there is a list of items in the ItemsSource, and the SelectedItem can be set to one of those items. Some other examples are ComboBoxes (Silverlight/UWP/WPF), and Pickers (Xamarin Forms).
In some cases, these controls are async ready, and in other cases, code needs to be written in order to handle scenarios where the ItemsSource is populated later than the SelectedItem. In our case, most of the time, the BindingContext (which contains the property bound to SelectedItem) will be set before the ItemsSource. So, we need to write code to allow this to function correctly. We have done this for ComboBoxes in Silverlight for example.
In Xamarin Forms, the ListView control is not async ready, i.e. if the ItemsSource is not populated before the SelectedItem is set, the selected item will never be highlighted on the control. This is probably by design and this is OK. The point of this thread is to find a way make the ListView async ready so that the ItemsSource can be populated after the SelectedItem is set.
There should be straight forward work arounds that can be implemented on other platforms to achieve this, but there are a few bugs in the Xamarin Forms list view that make it seemingly impossible to work around the issue. The sample app I have created is shared between WPF and Xamarin Forms in order to show how the ListView behaves differently on each platform. The WPF ListView for example, is async ready. If the ItemsSource is populated after the DataContext is set on a WPF ListView, the SelectedItem will bind to the item in the list.
In Xamarin Forms, I cannot consistently get SelectedItem two way binding on ListView to work. If I select an item in the ListView, it sets the property on my model, but if I set the property on my model, the item that should be selected is not reflected as being selected in the ListView. After the items have been loaded, when I set the property on my model, no SelectedItem is displayed. This is happening on UWP and Android. iOS remains untested.
You can see the sample problem in this Git repo:
https://ChristianFindlay#bitbucket.org/ChristianFindlay/xamarin-forms-scratch.git . Simply run the UWP, or Android sample, and click Async ListView. You can also run the XamarinFormsWPFComparison sample to see how the WPF version behaves differently.
When you run the Xamarin Forms sample, you will see that there is no item selected after the items load. However in the WPF version, it is selected. Note: It's not highlighted blue, but it is slightly grey indicating that it is selected. This is where my problem is, and the reason I can't work around the async issue.
Here is my code (clone repo for absolute latest code):
public class AsyncListViewModel : INotifyPropertyChanged
{
#region Fields
private ItemModel _ItemModel;
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Public Properties
public ItemModel ItemModel
{
get
{
return _ItemModel;
}
set
{
_ItemModel = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ItemModel)));
}
}
#endregion
}
public class ItemModel : INotifyPropertyChanged
{
#region Fields
private int _Name;
private string _Description;
#endregion
#region Events
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Public Properties
public int Name
{
get
{
return _Name;
}
set
{
_Name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public string Description
{
get
{
return _Description;
}
set
{
_Description = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Description)));
}
}
#endregion
#region Public Methods
public override bool Equals(object obj)
{
var itemModel = obj as ItemModel;
if (itemModel == null)
{
return false;
}
var returnValue = Name.Equals(itemModel.Name);
Debug.WriteLine($"An {nameof(ItemModel)} was tested for equality. Equal: {returnValue}");
return returnValue;
}
public override int GetHashCode()
{
Debug.WriteLine($"{nameof(GetHashCode)} was called on an {nameof(ItemModel)}");
return Name;
}
#endregion
}
public class ItemModelProvider : ObservableCollection<ItemModel>
{
#region Events
public event EventHandler ItemsLoaded;
#endregion
#region Constructor
public ItemModelProvider()
{
var timer = new Timer(TimerCallback, null, 3000, 0);
}
#endregion
#region Private Methods
private void TimerCallback(object state)
{
Device.BeginInvokeOnMainThread(() =>
{
Add(new ItemModel { Name = 1, Description = "First" });
Add(new ItemModel { Name = 2, Description = "Second" });
Add(new ItemModel { Name = 3, Description = "Third" });
ItemsLoaded?.Invoke(this, new EventArgs());
});
}
#endregion
}
This is the XAML:
<Grid x:Name="TheGrid">
<Grid.Resources>
<ResourceDictionary>
<local:ItemModelProvider x:Key="items" />
</ResourceDictionary>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="100" />
</Grid.RowDefinitions>
<ListView x:Name="TheListView" Margin="4" SelectedItem="{Binding ItemModel, Mode=TwoWay}" ItemsSource="{StaticResource items}" HorizontalOptions="Center" VerticalOptions="Center" BackgroundColor="#EEEEEE" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Label Text="{Binding Name}" TextColor="#FF0000EE" VerticalOptions="Center" />
<Label Text="{Binding Description}" Grid.Row="1" VerticalOptions="Center" />
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ActivityIndicator x:Name="TheActivityIndicator" IsRunning="True" IsVisible="True" Margin="100" />
<StackLayout Grid.Row="1" Orientation="Horizontal">
<Label Text="Name: " />
<Label Text="{Binding ItemModel.Name}" />
<Label Text="Description: " />
<Label Text="{Binding ItemModel.Description}" />
<Button Text="New Model" x:Name="NewModelButton" />
<Button Text="Set To 2" x:Name="SetToTwoButton" />
</StackLayout>
</Grid>
Code Behind:
public partial class AsyncListViewPage : ContentPage
{
ItemModelProvider items;
ItemModel two;
private AsyncListViewModel CurrentAsyncListViewModel => BindingContext as AsyncListViewModel;
public AsyncListViewPage()
{
InitializeComponent();
CreateNewModel();
items = (ItemModelProvider)TheGrid.Resources["items"];
items.ItemsLoaded += Items_ItemsLoaded;
NewModelButton.Clicked += NewModelButton_Clicked;
SetToTwoButton.Clicked += SetToTwoButton_Clicked;
}
private void SetToTwoButton_Clicked(object sender, System.EventArgs e)
{
if (two == null)
{
DisplayAlert("Wait for the items to load", "Wait for the items to load", "OK");
return;
}
CurrentAsyncListViewModel.ItemModel = two;
}
private void NewModelButton_Clicked(object sender, System.EventArgs e)
{
CreateNewModel();
}
private void CreateNewModel()
{
//Note: if you replace the line below with this, the behaviour works:
//BindingContext = new AsyncListViewModel { ItemModel = two };
BindingContext = new AsyncListViewModel { ItemModel = GetNewTwo() };
}
private static ItemModel GetNewTwo()
{
return new ItemModel { Name = 2, Description = "Second" };
}
private void Items_ItemsLoaded(object sender, System.EventArgs e)
{
TheActivityIndicator.IsRunning = false;
TheActivityIndicator.IsVisible = false;
two = items[1];
}
}
Note: if I change the method CreateNewModel to this:
private void CreateNewModel()
{
BindingContext = new AsyncListViewModel { ItemModel = two };
}
the SelectedItem is reflected on screen. This seems to indicate that the ListView is comparing items based on object reference as opposed to using the Equals method on the objects. I tend to think of this as a bug. But, this is not the only issue here, because if this were the only issue, then clicking the SetToTwoButton should yield the same result.
It is now clear that there are several bugs around this is Xamarin Forms. I have documented the repro steps here:
https://bugzilla.xamarin.com/show_bug.cgi?id=58451

The AdaptListView is a suitable alternative to the ListView control and isn't subject to these issues.

The Xamarin Forms team created a pull request to solve some of the issues here:
https://github.com/xamarin/Xamarin.Forms/pull/1152
But, I don't believe this pull request was ever accepted in to the master branch of Xamarin Forms.

Related

Listview doesn't refresh the item view correctly

I'm developing an app with xamarin forms and the MVVM pattern. I have a page with a listview that has three buttons but all the time with only 2 visibles and change the visibility of two of them when I press a button. The problem is that for the first ten items it works like supposed to be, press the button and dissapear and appear the other, but after the 10th item when I press the button it dissapear but the other doesn't appear until I scrool the list view to a position where the item is out of the screen. When the item is out of the screen and come back to be on the screen, the button appear. The visibility of the buttons is controlled changing a boolean property that is binded to the IsVisible property of the button and one of them with a converter to negate the value of the property. This is a repository that you can clone and see the code and test, maybe is something with my Visual Studio.
Initially, I thought it could be for a race condition and made the method that change the variable synchronous but it doesn't work.
This is my list view
<ListView ItemsSource="{Binding Items}"
HasUnevenRows="True"
SeparatorVisibility="None"
IsRefreshing="False">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Name}"/>
<StackLayout Orientation="Horizontal">
<Button Text="One"
HorizontalOptions="CenterAndExpand"
TextColor="Green"
BackgroundColor="White"
BorderColor="Green"
BorderWidth="1"
WidthRequest="150" />
<Button Text="Two"
HorizontalOptions="CenterAndExpand"
BackgroundColor="Green"
TextColor="White"
Command="{Binding TestCommand}"
WidthRequest="150"
IsVisible="{Binding TestVariable, Converter={StaticResource negate}}" />
<Button Text="Three"
HorizontalOptions="CenterAndExpand"
BackgroundColor="Red"
Command="{Binding TestCommand}"
TextColor="White"
WidthRequest="150"
IsVisible="{Binding TestVariable}" />
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The viewmodel
public class ListViewTestModel : BaseViewModel
{
private List<ListItemTestModel> items;
public List<ListItemTestModel> Items
{
get => items;
set
{
SetValue(ref items, value);
}
}
public ListViewTestModel()
{
List<ListItemTestModel> itemList = new List<ListItemTestModel>();
for (int i = 0; i < 40; i++)
{
itemList.Add(new ListItemTestModel { Name = "Test" });
}
Items = itemList;
}
}
And another view model that is binded to each item in the listView
public class ListItemTestModel : BaseViewModel
{
private bool testVariable;
public string Name { get; set; }
public bool TestVariable
{
get
{
return testVariable;
}
set
{
SetValue(ref testVariable, value);
}
}
public Command TestCommand { get; set; }
public ListItemTestModel()
{
TestCommand = new Command(() =>
{
TestMethod();
});
}
public void TestMethod()
{
TestVariable = !TestVariable;
}
}
the BaseViewModel
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(ref T backingField, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(backingField, value))
{
return;
}
backingField = value;
OnPropertyChanged(propertyName);
}
}
And the codebehind of the page
public partial class MainPage : ContentPage
{
public ListViewTestModel ViewModel { get; }
public MainPage()
{
ViewModel = new ListViewTestModel();
BindingContext = ViewModel;
InitializeComponent();
}
}
I suggest listview Caching Strategy may case this issue, the default value is RetainElement for ListView, so using CachingStrategy="RecycleElement" in ListView.
About listview Caching Strategy, you can take a look:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/performance#caching-strategy
You should definitely go to ObservableCollection type for your items thus you'll be able to observe and display any changes
private ObservableCollection<ListItemTestModel> items;
public ObservableCollection<ListItemTestModel> Items
{
get => items;
set => SetValue(ref items, value);
}
And you should set your BindingContext AFTER the InitializeComponent() method or property changed will be propagate before your view is initialized.
public MainPage()
{
InitializeComponent();
BindingContext = new ListViewTestModel();;
}
public ListViewTestModel()
{
List<ListItemTestModel> itemList = new List<ListItemTestModel>();
for (int i = 0; i < 40; i++)
{
itemList.Add(new ListItemTestModel { Name = "Test" });
}
Items = new ObservableCollection<ListItemTestModel>(itemList);
}

C# UWP - Update UI to display object values that have been read in by the user

I have been tasked with creating my first UWP App in C#.
The basic idea is to read in an XML file and create objects based on the data read in, then display the properties stored in the object to users in the IU.
Lets say a Person object that has a name, age, and height. I want to display the Person fields after I have read in the data but I can't get anything to show up in the UI after creating the Person object.
I have created a Person class that holds the name, age, height. I have another class that extends ObservableCollection<> and a ItemTemplate that looks for the observable class but currently nothing is showing up on the UI.
Has anyone been through a similar process or know of the correct documentation to read?
Thanks.
First of all in UWP you can choose between two types of binding:
{x:Bind }, is slightly faster at compile time, binds to your Framework Element code-behind class, but it is not as flexible as the other type of binding.
The default mode for this type of binding is OneTime, therefore you will only have your data actually propagated onto your UI, when you construct your object.
{Binding }, in this type of binding where you can only reference variables which exists inside the DataContext of a parent element. The default mode is OneWay.
With that in mind, first of all dealing with a ViewModel which is just a bunch of properties, is different from actually dealing with a Collection, since I don't think the Collection can actually detect alterations on the items itself, but rather on its structure.
Therefore during the Add/Remove process of items in your Collection, you have to actually subscribe/unsubscribe those items to the PropertyChanged EventHandler.
Nevertheless with the following code, i think you should be able to start visualizing updates onto your UI:
VIEWMODEL
public class PersonsObservable<T> : ObservableCollection<Person> where T : INotifyPropertyChanged
{
private PersonsObservable<Person> _personslist;
public PersonsObservable<Person> personslist
{
get { return _personslist; }
set
{
_personslist = value;
_personslist.CollectionChanged += OnObservableCollectionChanged;
}
}
public void OnObservableCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e.NewItems != null)
{
foreach (object item in e.NewItems)
((INotifyPropertyChanged)item).PropertyChanged += OnItemPropertyChanged;
}
if(e.OldItems != null)
{
foreach (object item in e.OldItems)
((INotifyPropertyChanged)item).PropertyChanged -= OnItemPropertyChanged;
}
}
public void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
NotifyCollectionChangedEventArgs args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((Person)sender));
OnCollectionChanged(args);
}
}
public class Person : INotifyPropertyChanged
{
public Person()
{
_name = "Walter White";
_age = 40;
_height = 180;
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private string _name;
public string name
{
get
{
return _name;
}
set
{
_name = value;
this.OnPropertyChanged();
}
}
private int _age;
public int age
{
get
{
return _age;
}
set
{
_age = value;
this.OnPropertyChanged();
}
}
private int _height;
public int height
{
get
{
return _height;
}
set
{
_height = value;
this.OnPropertyChanged();
}
}
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// Add Items
PersonsList.Add(new Person());
}
}
XAML
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Orientation="Vertical">
<TextBlock Text="DataBinding" Foreground="DarkBlue" FontSize="18" FontWeight="Bold"/>
<ItemsControl ItemsSource="{Binding Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Name: "/>
<TextBlock Text="{Binding name, Mode=TwoWay}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Age: "/>
<TextBlock Text="{Binding age, Mode=TwoWay}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Height: "/>
<TextBlock Text="{Binding height, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Add Items" Click="Button_Click" Background="Blue" VerticalAlignment="Bottom"/>
</StackPanel>
</Grid>
*Test adding items *
private void Button_Click(object sender, RoutedEventArgs e)
{
// Add Items
PersonsList.Add(new Person());
}
Expose your property and set it to the DataContext of your page (with x:Bind you wouldn't need to do this, but instead you would have to perform a cast for your code to actually compile).
public MainPage()
{
InitializeComponent();
PersonsList = new PersonsObservable<Person>();
this.DataContext = PersonsList;
PersonsList.Add(new Person());
PersonsList.Add(new Person());
}
PersonsObservable<Person> PersonsList { get; set; }
I haven't tested for the situation where one of the items is altered, but you can easily do that, by adding another button (and click event) and actually test if changing one of the items's properties update in your UI.
Anything else, feel free to ask, will be glad to help!

Getting IsChecked Property of a CheckBox in a ListBox

So many examples found and none fit! My list box is a list of Result objects. Results can be checked or unchecked in a listbox to mark them as 'Allowed to 'transmit.
<ListBox
x:Name="FileListBox"
ItemsSource="{Binding TestResults}"
ItemTemplate="{StaticResource FileListTemplate}"
SelectionMode="Single"
SelectedItem="{Binding FileListSelected}"
Background="#FFFFFBE2" />
The FileListTemplate
<DataTemplate x:Key="FileListTemplate">
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*" />
<ColumnDefinition Width=".3*" />
<ColumnDefinition Width=".2*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding FileName}" />
<TextBlock Grid.Column="1"
Text="Machine">
</TextBlock>
<CheckBox x:Name="UploadOK"
Grid.Column="2"
HorizontalAlignment="Right"
IsChecked="{Binding CanUpload, Mode=TwoWay}" />
</Grid>
</DataTemplate>
I took out a lot of formatting code to reduce the clutter. So when the check box is checked (or un checked) I need to set a boolean on the object to true or false. But I do not want the ListItem selected just because the checkbox is selected. When the ListItem is selected something else happens. Here is the code for that.
public TestResult FileListSelected
{
get
{
return selectedItem;
}
set
{
if (value == selectedItem)
return;
selectedItem = value;
if (!Workspaces.Any(p => p.DisplayName == value.FileName))
{
this.DisplayTestResult(value as TestResult);
}
base.RaisePropertyChanged("FileListSelected");
}
}
And here is the code I bound to for the Checkbox (although it didn't work).
public bool CanUpload
{
get { return selectedItem.CanUpload; }
set
{
selectedItem.CanUpload = value;
}
}
I appreciate you looking at this.
Internal Class TestResult
{
...
private bool _canUpload;
public bool CanUpload
{
get { return _canUpload; }
set
{
_canUpload = value;
base.RaisePropertyChanged("CanUpload");
}
}
}
When working with MVVM always check for the following:
Add using System.ComponentModel; to your ViewModelClass
Inherit from INotifyPropertyChanged
Always check your DataContext and see the Output Window for BindingErrors
Create Bindings like this:
Example Property:
public string Example
{
get { return _example; }
set
{
_example= value;
OnPropertyChanged();
}
}
this will call OnPropertyChanged automatically every time a new value is assigned (not updated automaticaly once it changes from some other location!)
Make sure your Implementation of INotifyPropertyChanged looks like this:
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
for that you also need using System.Runtime.CompilerServices;
Other options to get your code working:
Your TestResults sould be an ObservableCollection<TestResult>
TestResult should have a property for CanUpload and FileName and inherit from INotifyPropertyChanged
Then on your MainViewModel for example on and ButtonClick your can get the selected files like this:
private List<string> GetSelectedFiles()
{
return TestResults.Where(result => result.CanUpload == true).Select(r => r.FileName).ToList());
}
Note:
FileListSelected is a Property of your ListBox's DataContext which is different to the DataContext of an entry (or at least should be).
FileListSelected will then return the selected Item of your ItemsSource.
Maybe you can comment on this problem with the row selection/checkbox check and add some detail so I can help you more.
EDIT: Notify MainWindowViewModel about CheckBox State Changes:
I see two possible approaches here:
USING EVENT
Add this to your TestResult class:
public delegate void CheckBoxStateChangedHandler(object sender, CheckBoxStateChangedEventArgs e);
public event CheckBoxStateChangedHandler CheckBoxStateChanged;
public class CheckBoxStateChangedEventArgs
{
bool CheckBoxChecked { get; set; }
}
Make sure that on creation of a new TestResult in your MainViewModel you subscribe to that event;
testResult.CheckBoxStateChanged += CheckBox_StateChanged;
Handle what you want to do once the state is changed in CheckBox_StateChanged. Note that the argument e contains the boolean (Checked) and the corresponding TestResult as the sender.
You simply invoke your new Event in the Setter of your CheckBox.Checked Binding:
public bool Checked
{
get { return _checked; }
set
{
_checked = value;
OnPropertyChanged();
CheckBoxStateChanged.Invoke(this, new CheckBoxStateChangedEventArgs() { CheckBoxChecked = value })
}
}
CALL METHOD ON MAINWINDOWVIEWMODEL
for that you need o create a static object of your MainWindowViewModel (in your MainViewModel) - don't forget to assigne a value once you create your MainWindowViewModel.
public static MainViewModel Instance { get; set; }
then simply add a public Method as you need:
public void CheckBoxValueChanged(bool value, TestResult result)
{
//Do whatever
}
you can also call in from the same spot as the event from above is invoked.
public bool Checked
{
get { return _checked; }
set
{
_checked = value;
OnPropertyChanged();
MainWindowViewModel.Instance.CheckBoxValueChanged(value, this);
}
}

Connect UIElements dynamically added in WPF using MVVM

I have UserControl with ItemsControl binded to ObservableCollection. DataTemplate in this ItemsControl is a Grid containing TextBox and Button.
Here is some code (Updated):
<UserControl.Resources>
<entities:SeparatingCard x:Key="IdDataSource"/>
</UserControl.Resources>
<ItemsControl ItemsSource="{Binding Cards}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Text="{Binding Id, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" GotFocus="TextBox_GotFocus" Grid.Row="0" Grid.Column="0"/>
<Button DataContext="{Binding Source={StaticResource IdDataSource}}" Command="{Binding Accept}" Grid.Row="0" Grid.Column="1">Accept</Button>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In model file:
public ObservableCollection<SeparatingCard> Cards { get; set; }
Card class:
class SeparatingCard : INotifyPropertyChanged
{
private string _id;
public string Id
{
get { return _id; }
set
{
_id = value;
OnPropertyChanged("Id");
}
}
public ActionCommand Accept { get; }
public SeparatingCard()
{
Accept = new ActionCommand(AcceptCommandExecute);
}
private void AcceptCommandExecute(object obj)
{
MessageBox.Show(Id);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Cards are added in runtime and I dynamically get a new textbox-button pair in my UserControl. Now in each pair I need to do the folowing things:
- Be able to check if the text in textbox is correct and disable/enable apropriate button.
- On button click get the text from apropriate textbox and process it.
I'd like all of this done via MVVM. But I only came to solution that directly have access to UI and implements only the second task:
private void Button_Click(object sender, RoutedEventArgs e)
{
var text = (((sender as Button).Parent as Grid).Children
.Cast<UIElement>()
.First(x => Grid.GetRow(x) == 0 && Grid.GetColumn(x) == 0) as TextBox).Text;
MessageBox.Show(text);
}
Update
As was suggested I tried to move ICommand logic to SeparatingCard class. Now it's always return null and I can't check what object of SeparatingCard class my command refers to. Updates are in the code above.
Instead of using Button.Click, Use Button.Command, which you can bind to some command in SeparatingCard.
Please have a look in this tutorial:
http://www.codeproject.com/Tips/813345/Basic-MVVM-and-ICommand-Usage-Example
Then, SeparatingCard ViewModel will contain an ICommand object which you can bind to Button.Command.
So if the user clicks the button, the event will be directed to the corresponding SeparatingCard object's command.

Textblock binding does not update in RunTime

I'm new in c# UWP development and I'm trying to change the value of a TextBlock in runtime, but the binding does not work properly.
I'm binding the text property of the TextBlock in XAML to a property on a ViewModel with INotifyPropertyChanged, and the value changes every 10 seconds.
I don't know if it's the correct way to do it, can someone help me?
Thanks in advance!
this is the ViewModel code
class MainPaigeViewModel : INotifyPropertyChanged
{
public MainPaigeViewModel()
{
Task.Run(async () =>
{
Random random = new Random();
while (true)
{
await Task.Delay(10000);
int newValue = random.Next(-40, 40);
_MyValue = newValue.ToString();
Debug.WriteLine(MyValue);
}
});
}
//Properties
private string _MyValue;
public string MyValue
{
get { return _MyValue; }
set
{
_MyValue = value;
RaisePropertyChanged("MyValue");
}
}
//INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
 and the XAML code
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:CountDown2"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ViewModels="using:CountDown2.ViewModels"
x:Class="CountDown2.MainPage"
mc:Ignorable="d">
<Page.DataContext>
<ViewModels:MainPaigeViewModel/>
</Page.DataContext>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<RelativePanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="{Binding MyValue, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
Width="100"
Height="40"
TextAlignment="Center"
FontSize="20"
/>
</RelativePanel>
</Grid>
</Page>
In UWP unlike silver light and WPF the default binding is One time for performance reasons. The Binding only takes place once as the application starts up. One way binding is the default of WinRT, Silverlight and wpf. Meaning the view will be updated but updating the view will not update view model. Two way binding will update both the view and the view model.
So for a <TextBlock> in the example, it is recommended to use One Way binding.
In a <TextBox> it is recommended to use Two Way binding for user input.
I found a couple small bugs that were causing the binding to fail ... so I changed the viewmodel... The private property was being used rather than public one. Since the code is updating the value in a thread, and then trying to marshal the objects across threads, a dispatcher was added. Also added a common base class for all view models. This make property binding a little easier, it stops binding issues when refactoring property names.
Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync
public class MainPaigeViewModel: ViewModelBase
{
public MainPaigeViewModel()
{
Task.Run(async () =>
{
Random random = new Random();
while (true)
{
await Task.Delay(1000);
int newValue = random.Next(-40, 40);
try
{
await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
() => {
MyValue = newValue.ToString();
});
}
catch (Exception ex)
{
string s = ex.ToString();
}
Debug.WriteLine(MyValue);
}
});
}
//Properties
private string _MyValue;
public string MyValue
{
get { return _MyValue; }
set
{
_MyValue = value;
OnPropertyChanged();
}
}
}
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I also changed the view to use x:binding. I like x:binding over the old data binding because it shows binding issues at compile time rather than at runtime. This is besides the performance enhancements it gives.
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<RelativePanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="{x:Bind viewModel.MyValue, Mode=OneWay}"
Width="100"
Height="40"
TextAlignment="Center"
FontSize="20"
/>
</RelativePanel>
</Grid>
Page behind code for x:bind
public sealed partial class MainPage : Page
{
public MainPaigeViewModel viewModel;
public MainPage()
{
this.InitializeComponent();
viewModel = new MainPaigeViewModel();
}
}
Try:Text="{Binding MyValue, Mode=TwoWay}"

Categories