Scenario - There is a ListView binded to a ObservableCollection of string. Listview has one label and one UserControl (containing nothing but a label). Both are binded to the same collection.
Also, there is a button which generate some random data for the collection.
Problem is when I run the app and click on Generate Data button the label gets updated but not the UserControl.
Below is the sample code.
MainPage.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:TestSample"
xmlns:controls="clr-namespace:TestSample.Controls"
x:Class="TestSample.MainPage">
<StackLayout>
<Button Text="Generate Data" Clicked="Button_Clicked"/>
<ListView Grid.Row="1" HorizontalOptions="Center" ItemsSource="{Binding Collection}" SeparatorVisibility="None">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal" HorizontalOptions="Center">
<Label Text="{Binding}"/>
<Label Text=" - "/>
<controls:MagicBox Text="{Binding}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
MainPage.xaml.cs
public partial class MainPage : ContentPage
{
public ObservableCollection<string> Collection { get; set; }
public MainPage()
{
InitializeComponent();
Collection = new ObservableCollection<string>
{
"XX",
"XX",
"XX"
};
this.BindingContext = this;
}
public void Button_Clicked(object sender, EventArgs e)
{
var rand = new Random();
for (int i = 0; i < 3; i++)
{
Collection[i] = rand.Next(10, 100).ToString();
}
}
}
UserControl
<ContentView.Content>
<Grid>
<Label Text="{Binding Text}" />
</Grid>
public partial class MagicBox : ContentView
{
public static readonly BindableProperty TextProperty =
BindableProperty.Create("Text", typeof(string), typeof(MagicBox), "XX");
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public MagicBox ()
{
InitializeComponent ();
this.BindingContext = this;
}
}
I also tried with ObservableCollection of a POCO class instead of string after implementing INotifyPropertyChanged, didn't worked.
If I bind the MagicBox Text to a string directly it works but not if I bind it to some property.
doing
this.BindingContext = this;
in MagicBox.xaml.cs forces the BindingContext to the current object. It also means that the BindingContext from the parent is no longer inherited.
in order to make it work, change your code behind to
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MagicBox : ContentView
{
public static readonly BindableProperty TextProperty =
BindableProperty.Create("Text", typeof(string), typeof(MagicBox), default(string));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public MagicBox ()
{
InitializeComponent ();
}
}
and your xaml to
<?xml version="1.0" encoding="UTF-8"?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TestSample.Controls.MagicBox"
x:Name="box">
<ContentView.Content>
<Grid>
<Label Text="{Binding Text, Source={x:Reference box}}" />
</Grid>
</ContentView.Content>
</ContentView>
I tested it. it works.
I think the problem is the line "this.BindingContext = this;" in your custom control.
You should Bind like this:
Text="{Binding Path=BindingContext, Source={x:Reference ListViewName}}"
Make sure add x:Name to your Listview. No tested, but hope it help you.
First, update your custom control:
Change your "Text" dependency property definition => Set the binding mode to "OneWay" and add propertyChanged event handler like this:
public partial class MagicBox : ContentView
{
public static readonly BindableProperty TextProperty =
BindableProperty.Create("Text", typeof(TextVM), typeof(MagicBox), "XX", BindingMode.OneWay, null, new BindableProperty.BindingPropertyChangedDelegate(TextPropertyChanged));
public TextVM Text
{
get { return (TextVM)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
Then add the 'Text' propertyChanged method into your custom control like this:
private static void TextPropertyChanged(BindableObject sender, object oldValue, object newValue )
{
Label updatedLabel = sender as Label;
if(updatedLabel == null) return;
updatedLabel.Text = (newValue as TextVM)?.MyText;
}
Make an Observable object that embed the text property, in order to throw the 'PropertyChanged' event:
public class TextVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _myText;
public string MyText
{
get => _myText;
set
{
_myText = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("MyText"));
}
}
}
Then in your XAML, update the text binding:
<controls:MagicBox Text="{Binding MyText}"/>
Don't forget to update your collection type and the random number generation process...
It should be good !
Related
I use FreshMvvm to develop and run MAUI project on Windows.
But I have some binding issues with ListView and my custom template.
The following is my code:
Model:
public class BaseModel
{
public string Code{ get; set; }
}
public class NameModel: BaseModel
{
public string Name{ get; set; }
}
ViewModel:
public class MainPageModel : FreshBasePageModel
{
private readonly IApiService _apiService;
private List<NameModel> _nameModelList;
public List<NameModel> NameModelList
{
get => _nameModelList;
private set
{
_nameModelList= value;
RaisePropertyChanged(nameof(NameModelList));
}
}
public MainPageModel(IApiService apiService)
{
_apiService = apiService;
}
protected override void ViewIsAppearing(object sender, EventArgs e)
{
base.ViewIsAppearing(sender, e);
Task.Run(() => GetNameData());
}
private async Task GetNameData()
{
var result = await _apiService.GetNameData();
NameModelList= result.GetRange(1, 10);
}
}
I create a list and use an api service to get a name model list data.
If api service gets the data, NameModelList will be updated.
NameModelList is the property which will be bind on Listview.ItemsSource
MainPage.xmal:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyNamespace.ViewCells.CustomListViewCell"
x:Class="MyNamespace.Pages.MainPage"
BackgroundColor="{DynamicResource SecondaryColor}">
<Grid RowSpacing="25"
RowDefinitions="Auto"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<ListView
x:Name="MyListView"
ItemsSource="{Binding NameModelList}"
Grid.Row="0"
WidthRequest="800"
HeightRequest="800"
BackgroundColor="Gray"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand">
<ListView.ItemTemplate>
<DataTemplate>
<local:MyCustomViewCell/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ContentPage>
Custom ViewCell (.xml):
<ViewCell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyNamespace.ViewCells.CustomListViewCell.MyCustomViewCell">
<Grid RowSpacing="100" WidthRequest="100" HeightRequest="100">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100*" />
</Grid.ColumnDefinitions>
<StackLayout
GridLayout.Row="0"
GridLayout.Column="0">
<Label
Text="{Binding Code}"
FontSize="30"/>
<Label
Text="{Binding Name}"
FontSize="30"/>
</StackLayout>
</Grid>
</ViewCell>
Custom ViewCell (.cs)
public partial class MyCustomViewCell: ViewCell
{
public static readonly BindableProperty CodeProperty =
BindableProperty.Create("Code", typeof(string), typeof(MyCustomViewCell), "");
public string Code
{
get { return (string)GetValue(CodeProperty); }
set { SetValue(CodeProperty, value); }
}
public static readonly BindableProperty NameProperty =
BindableProperty.Create("Name", typeof(string), typeof(MyCustomViewCell), "");
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
}
I define a custom ViewCell files and put this ViewCell in the Listview of MainPage.
Now my question is my Listview can't show data successfully.
I'm sure that NameModelList has value and its count is more than 1.
But I can see nothing.
The output log has no error, and the breakpoints in MyCustomViewCell.cs are never triggered.
So I think I have some binding issues, but I can't find it out.
To get to the bottom of this I took your code and put it in a project so I could have a little play with it. You can find the repo here. Not to be rude here or anything, but might be a good idea for a next question to do that yourself, that will help speed things up :)
Anyway, the problem is much more subtle. Because you're using XAML for your layout, you'll have to call InitializeComponent in the constructor. So adding this to your MyCustomViewCell made it work:
public MyCustomViewCell()
{
InitializeComponent();
}
I have searched around and I dont think I am finding the answer to my question. I am new to xamarin so i hope I am using the correct terminology. I am experimenting with custom cells in listviews. My aim is to reuse the custom cell throughout multiple parts of my application but when I use the event "ItemSelected" it comes back with the bindings to the custom cell and not my original listview itemsource bindings. I understand why I think but I am unsure how to bind the ItemSelected to the original source. Am I using the right method here? I am completely lost if I am honest.
This is my custom cell code:
public partial class ListCell : ViewCell
{
public static readonly BindableProperty LabelHeaderProperty = BindableProperty.Create("LabelHeader", typeof(string), typeof(ListCell));
public string LabelHeader
{
get { return (string)GetValue(LabelHeaderProperty); }
set { SetValue(LabelHeaderProperty, value); }
}
public static readonly BindableProperty LabelSmallProperty = BindableProperty.Create("LabelSmall", typeof(string), typeof(ListCell));
public string LabelSmall
{
get { return (string)GetValue(LabelSmallProperty); }
set { SetValue(LabelSmallProperty, value); }
}
public ListCell()
{
InitializeComponent();
}
protected override void OnAppearing()
{
base.OnAppearing();
BindingContext = new
{
LabelHeader = this.LabelHeader,
LabelSmall = this.LabelSmall
};
}
}
Here is my ListView
<ListView x:Name="MyListView"
ItemsSource="{Binding Items}"
VerticalOptions="FillAndExpand"
HasUnevenRows="true"
IsPullToRefreshEnabled="true"
ItemSelected="OnItemSelected"
SeparatorVisibility="None">
<ListView.ItemTemplate>
<DataTemplate>
<extensions:ListCell LabelHeader="{Binding Description}"
LabelSmall="{Binding Description}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Thank you very much in advance :)
According to your code, when binding to a custom cell type's BindableProperty instances, the UI controls displaying the BindableProperty values should use the OnBindingContextChanged override to set the data to be displayed in each cell.
public class ListCell:ViewCell
{
Label headerLabel, smallLabel;
public static readonly BindableProperty LabelHeaderProperty = BindableProperty.Create("LabelHeader", typeof(string), typeof(ListCell),"name");
public string LabelHeader
{
get { return (string)GetValue(LabelHeaderProperty); }
set { SetValue(LabelHeaderProperty, value); }
}
public static readonly BindableProperty LabelSmallProperty = BindableProperty.Create("LabelSmall", typeof(string), typeof(ListCell),"small label");
public string LabelSmall
{
get { return (string)GetValue(LabelSmallProperty); }
set { SetValue(LabelSmallProperty, value); }
}
public ListCell()
{
StackLayout stack = new StackLayout { Orientation=StackOrientation.Horizontal};
headerLabel = new Label { FontAttributes = FontAttributes.Bold };
smallLabel = new Label();
stack.Children.Add(headerLabel);
stack.Children.Add(smallLabel);
View = stack;
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (BindingContext != null)
{
headerLabel.Text = LabelHeader;
smallLabel.Text = LabelSmall;
}
}
}
<ListView
x:Name="listView"
ItemSelected="listView_ItemSelected"
ItemsSource="{Binding items}">
<ListView.ItemTemplate>
<DataTemplate>
<local:ListCell LabelHeader="{Binding Name}" LabelSmall="{Binding description}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
But You can use TextCell in ListView's DataTemplate directly, don't need to create custom viewcell.
<ListView ItemsSource="{Binding items}">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Detail="{Binding description}" Text="{Binding name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
About using TextCell, you can take a look:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/listview/data-and-databinding#binding-cells
I'm trying to create a custom control that contains a Picker With Xamarin.Forms.
the problem is when trying to bind the ItemSource property, it's never gets binded, and when I touch the custom control on the mobile it shows an empty dialog with no binded items.
Note: I tried almost every solution I found on "Stack OverFlow" or on "forums.xamarin", and none of them worked for me.
here's my code:
For the Custom Control XAML file - which's named with "HitPicker" - :
<Picker x:Name="PickerField"
HeightRequest="46"
TitleColor="{Binding TitleColor}"
TextColor="{Binding TextColor}"
BackgroundColor="{Binding BackgroundColor}"
Unfocused="Handle_Unfocused"
Focused="Handle_Focused"
SelectedItem="{Binding SelectedItem}"
ItemsSource="{Binding ItemsSource}">
</Picker>
For Custom Control cs File:
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(List<string>), typeof(HitPicker), default(List<string>), BindingMode.TwoWay, null, OnItemsSourceChanged);
public List<string> ItemsSource
{
get => (List<string>)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public HitPicker()
{
InitializeComponent();
BindingContext = this;
}
private static void OnItemsSourceChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var picker = (bindable as HitPicker).PickerField;
picker.Items.Clear();
var newList = newvalue as List<string>;
if (newvalue != null)
{
foreach (var item in newList)
{
picker.Items.Add(item.ToString());
}
}
}
knowing that OnItemsSourceChanged method is never called, and almost every similar question to mine is answered with a similar answer, that suggests putting this method in the control class.
for XAML file that uses this control:
<controls:HitPicker ItemsSource="{Binding MonkeyList}" Title="Select monky" BackgroundColor="Azure"></controls:HitPicker>
and here's the monkey list declaration in the ViewModel for the above XAML:
private List<string> _lst = new List<string>{
"Baboon",
"Capuchin Monkey",
"Blue Monkey",
"Squirrel Monkey",
"Golden Lion Tamarin",
"Howler Monkey",
"Japanese Macaque"
};
public List<string> MonkeyList
{
get => _lst;
set
{
_lst = value;
OnPropertyChanged();
}
}
MonkeyList getter is never called too, knowing that the Binding context is the ViewModel
When you set the bindingcontext in CustomControl like
public HitPicker()
{
InitializeComponent();
BindingContext = this;
}
It will break the binding between custom control and ContentPage .
So you could modify the code like following
in HitPicker.xaml
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
//...
x:Name="pickerView" //set name of page >
<Picker x:Name="PickerField"
HeightRequest="46"
TitleColor="{Binding Source={x:Reference pickerView}, Path=TitleColor}"
TextColor="{Binding Source={x:Reference pickerView}, Path=TextColor}"
BackgroundColor="{Binding Source={x:Reference pickerView}, Path=BackgroundColor}"
Unfocused="Handle_Unfocused"
Focused="Handle_Focused"
SelectedItem="{Binding Source={x:Reference pickerView}, Path=SelectedItem}">
</Picker>
in HitPicker.xaml.cs
public HitPicker()
{
InitializeComponent();
//BindingContext = this;
}
I've uploaded my sample project here: https://www.file-upload.net/download-13252079/WpfApp1.zip.html
It is a WPF window with several of the same usercontrols in it. The UserControl has a dependency property named "Text" which is bound to a MainWindowViewModel's property and successfully shows up in the UserControl's TextBlock.
However if I double-click the UserControl and want it to give the value of its dependency property, the value is null. Why is this?
Thanks a lot for your help!
Edit: sorry, here is some source code:
The UserControl's XAML:
<UserControl x:Class="WpfApp1.UserControl1"
...
x:Name="UC1">
<StackPanel Orientation="Vertical">
<TextBlock Margin="5" Text="Test" FontSize="20"></TextBlock>
<TextBlock Margin="5" Text="{Binding ElementName=UC1, Path=Text}" FontSize="20"></TextBlock>
</StackPanel>
</UserControl>
The UserControl's code:
public partial class UserControl1 : UserControl, INotifyPropertyChanged
{
public UserControl1()
{
InitializeComponent();
}
string text;
public string Text
{
get { return text; }
set { SetProperty(ref text, value); }
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
"Text", typeof(string), typeof(UserControl1));
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The main window's XAML:
<Window x:Class="WpfApp1.MainWindow"
...>
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<StackPanel>
<local:UserControl1 Text="{Binding Values[0]}"
MouseDoubleClick="UserControl1_MouseDoubleClick">
</local:UserControl1>
<local:UserControl1 Text="{Binding Values[1]}"
MouseDoubleClick="UserControl1_MouseDoubleClick">
</local:UserControl1>
<local:UserControl1 Text="{Binding Values[2]}"
MouseDoubleClick="UserControl1_MouseDoubleClick">
</local:UserControl1>
<local:UserControl1 Text="{Binding Values[3]}"
MouseDoubleClick="UserControl1_MouseDoubleClick">
</local:UserControl1>
</StackPanel>
</Window>
The main window's code behind:
private void UserControl1_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is UserControl1)
{
// why is (sender as UserControl1).Text null?
MessageBox.Show("Text is: " + (sender as UserControl1).Text);
}
}
The main window's view model:
class MainWindowViewModel : BindableBase
{
public MainWindowViewModel()
{
Values = new ObservableCollection<string>();
Values.Add("first string");
Values.Add("second string");
Values.Add("third string");
Values.Add("fourth string");
}
#region Values
private ObservableCollection<string> values;
public ObservableCollection<string> Values
{
get { return values; }
set { SetProperty(ref values, value); }
}
#endregion
}
Here is how your UserControl's code behind should look like. You do not need to implement INotifyPropertyChanged.
See Custom Dependency Properties for all the details. Specifically, you must call GetValue and SetValue (and nothing else) from the getter and setter of the Text property wrapper.
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
nameof(Text), typeof(string), typeof(UserControl1));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
}
For a Binding to the UserControl's Text property in its own XAML you could use RelativeSource instead of ElementName to save a useless generated class member:
<UserControl x:Class="WpfApp1.UserControl1" ...>
...
<TextBlock Text="{Binding Text,
RelativeSource={RelativeSource AncestorType=UserControl}}" .../>
...
</UserControl>
I have a MainWindow, when a button is pressed a methode in my ViewModel gets called.
public void BtnOpenClick()
{
//DoStuff
VideoControl mediaplayer = new VideoControl
}
which instantiats a new Videocontrol from my views which is a usercontrol written in some XAML.
My question is how can I return a usercontrol from the ViewModel back to the MainWindow in MVVM way?
Edit
I had a misunderstanding off the MVVM Pattern and what I was trying to do clearly violates the pattern.
For now I have a solution In the Mainwindow I bind to my usercontrol (VideoControl)
<StackPanel>
<Local:VideoControl IconInfos="{Binding SourceVideos}"/>
</StackPanel>
My ViewModel looks Like this
class VideoControlViewModel : ViewModel
{
private ObservableCollection<Video> _Videos;
public ObservableCollection<Video> Videos
{
get { return _Videos; }
set { SetProperty(ref _Videos, value); }
}
}
My Model is just the Uri of the File I want to play in my MediaElement
public class Video
{
public Uri FileName { get; set; }
}
Than in the usercontrol I have a datatemplate and Some More XAML
<UserControl.Resources>
<DataTemplate x:Key="VideoTemplate">
<MediaElement x:Name="MediaPlayer" Source="{Binding FileName }"/>
</DataTemplate>
</UserControl.Resources>
<StackPanel Orientation="Horizontal">
<ItemsControl ItemsSource="{Binding Videos}"
ItemTemplate="{StaticResource VideoTemplate}"
Grid.Row="1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
And in the Code Behind I have this code
VideoControlViewModel _vm;
public VideoControl()
{
InitializeComponent();
_vm = (VideoControlViewModel) VideoGrid.DataContext;
}
public ObservableCollection<Video> IconInfos
{
get { return (ObservableCollection<Video>)GetValue(IconInfosProperty); }
set { SetValue(IconInfosProperty, value); }
}
public static readonly DependencyProperty IconInfosProperty =
DependencyProperty.Register("IconInfos", typeof(ObservableCollection<Video>),
typeof(VideoControl), new PropertyMetadata(null, OnIconInfosSet));
private static void OnIconInfosSet(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
((VideoControl)d)._vm.Videos = e.NewValue as ObservableCollection<Video>;
}
For Now when I call a button I can add another Item to the Obeservable collection and a mediaplayer will pop up on the main window with the selected Uri.
Try to pass your control to the content:
public void BtnOpenClick()
{
//DoStuff
VideoControl mediaplayer = new VideoControl();
MainWindow.YourControl.Content = mediaplayer;
}