What i want is, when the value is changed, it should call CreateChart()and use the new values.
I try to call in an onPropertyChange method OnValueChanged a bindable Property with reflection, but the property is always null and i dont get the value of the property Value
public partial class CorrectWrongRingChart : ContentView
{
public CorrectWrongRingChart()
{
InitializeComponent();
}
public static readonly BindableProperty ChartProperty =
BindableProperty.Create(
nameof(CorrectWrongChart),
typeof(Chart),
typeof(CorrectWrongRingChart));
public Chart CorrectWrongChart
{
get { return (Chart)GetValue(ChartProperty); }
set => SetValue(ChartProperty, value);
}
public static readonly BindableProperty ValueProperty =
BindableProperty.Create(
nameof(Value),
typeof(CorrectWrongValue),
typeof(CorrectWrongRingChart),
propertyChanged: OnValueChanged);/*(b, o, n) => { ((CorrectWrongRingChart)b).OnPropertyChanged("Text");});*/
public CorrectWrongValue Value
{
get { return (CorrectWrongValue)GetValue(ValueProperty); }
set => SetValue(ValueProperty, value);
}
private static void OnValueChanged(BindableObject bindable, object oldValue, object newValue)
{
((CorrectWrongRingChart)bindable).OnPropertyChanged("Text");
//((CorrectWrongRingChart)bindable).OnPropertyChanged("Correct");
//((CorrectWrongRingChart)bindable).OnPropertyChanged("Wrong");
var valueProperty = ValueProperty.GetType().GetProperty("Value");
var value = (CorrectWrongValue)valueProperty.GetValue("Value");
var ChartProperty = ValueProperty.GetType().GetProperty("CorrectWrongChart");
if (value != null)
{
ChartProperty.SetValue("CorrectWrongChart", CreateChart(value));
}
}
public static readonly BindableProperty TextProperty =
BindableProperty.Create(
nameof(Text),
typeof(string),
typeof(CorrectWrongRingChart),
defaultValue: string.Empty);
public string Text => $"{Value?.CorrectCount ?? 0}/{Value?.TotalCount ?? 0}";
//public double Correct => Value.CorrectPercentage;
//public double Wrong => Value.WrongPercentage;
private static Chart CreateChart(CorrectWrongValue value)
{
var chart = new Microcharts.DonutChart();
chart.IsAnimated = false;
ChartEntry corretEntry = new ChartEntry((float)value.CorrectPercentage)
{
Color = SKColor.Parse("#00FF00")
};
ChartEntry wrongEntry = new ChartEntry((float)value.WrongPercentage)
{
Color = SKColor.Parse("#FF0000")
};
chart.Entries = new List<ChartEntry>() { corretEntry, wrongEntry };
return chart;
}
}
Xaml:
<Grid >
<forms:ChartView x:Name="chart1" WidthRequest="130" HeightRequest="130" Chart="{Binding CorrectWrongChart, Source={x:Reference Root}}">
</forms:ChartView>
<Label Text="{ Binding Text, Source={x:Reference Root} }"
VerticalOptions="Center"
HorizontalOptions="Fill"
HorizontalTextAlignment="Center"
TextColor="Black"
FontSize="19"
FontFamily="{ StaticResource AppBoldFontFamily }" />
</Grid>
If you want to get Value when the value is changed , you could get it directly like following
private static void OnValueChanged(BindableObject bindable, object oldValue, object newValue)
{
var currentValue = newValue;
// do something you want
// you could get other property like following
// var view = bindable as CorrectWrongRingChart;
// var currentText = view.Text;
}
The property Value will change automatically when we change the value of it source .So we don't need to invoke the following lines any more , which maybe will lead to infinite loop .
if (value != null)
{
ChartProperty.SetValue("CorrectWrongChart", CreateChart(value));
}
Related
I'm trying to learn WinUI by building an app that can help with planning holidays. One of the things I'm trying to do is build a UI that shows a list of destinations (currently using a ListView), and then have a button to the right essentially pointing to the line between them that could pop up a UI for 'travel' between the two destinations.
This is a mockup of what I'm trying to achieve:
The buttons with the badly drawn plane and train icons should represent the travel between the two locations and therefore should be in the middle.
I've mainly been flailing about trying to get it to work using a grid inside of the DataTemplate, but haven't been able to get the 'half-way' alignment without making the gap between the destinations bigger.
Does anyone have any suggestions as to how I could achieve this, or is it a fools errand?
I guess you need to create a user control (or a custom control). Something like this:
CustomList.xaml
<UserControl
x:Class="UserControls.CustomList"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:UserControls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid x:Name="RootContainer" />
</UserControl>
CustomList.xaml
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using System.Collections.Generic;
using Windows.Foundation.Collections;
namespace UserControls;
public sealed partial class CustomList : UserControl
{
public static readonly DependencyProperty MainItemsProperty = DependencyProperty.Register(
nameof(MainItems),
typeof(IEnumerable<string>),
typeof(CustomList),
new PropertyMetadata(default, (d, e) => (d as CustomList)?.OnItemsPropertyChanged()));
public static readonly DependencyProperty SubItemsProperty = DependencyProperty.Register(
nameof(SubItems),
typeof(IEnumerable<string>),
typeof(CustomList),
new PropertyMetadata(default, (d, e) => (d as CustomList)?.OnItemsPropertyChanged()));
public CustomList()
{
this.InitializeComponent();
this.RootContainer.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Auto) });
this.RootContainer.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Auto) });
}
public IEnumerable<string> MainItems
{
get => (IEnumerable<string>)GetValue(MainItemsProperty);
set => SetValue(MainItemsProperty, value);
}
public IEnumerable<string> SubItems
{
get => (IEnumerable<string>)GetValue(SubItemsProperty);
set => SetValue(SubItemsProperty, value);
}
private ICollectionView? MainItemsCollectionView { get; set; }
private ICollectionView? SubItemsCollectionView { get; set; }
private void OnItemsPropertyChanged()
{
CollectionViewSource mainCollectionViewSource = new()
{
Source = MainItems
};
if (MainItemsCollectionView is not null)
{
MainItemsCollectionView.VectorChanged -= ItemsCollectionView_VectorChanged;
}
MainItemsCollectionView = mainCollectionViewSource.View;
MainItemsCollectionView.VectorChanged += ItemsCollectionView_VectorChanged;
CollectionViewSource subCollectionViewSource = new()
{
Source = SubItems
};
SubItemsCollectionView = subCollectionViewSource.View;
RefreshList();
}
private void ItemsCollectionView_VectorChanged(IObservableVector<object> sender, IVectorChangedEventArgs #event)
{
RefreshList();
}
private void RefreshList()
{
this.RootContainer.Children.Clear();
this.RootContainer.RowDefinitions.Clear();
if (MainItemsCollectionView is not null)
{
UpdateRowDefinitions(MainItemsCollectionView.Count);
for (int i = 0; i < MainItemsCollectionView.Count; i++)
{
TextBlock mainItemTextBlock = new()
{
Text = MainItemsCollectionView[i] as string,
};
Grid.SetColumn(mainItemTextBlock, 0);
Grid.SetRow(mainItemTextBlock, i * 2);
Grid.SetRowSpan(mainItemTextBlock, 2);
this.RootContainer.Children.Add(mainItemTextBlock);
if (SubItemsCollectionView?.Count > i)
{
TextBlock subItemTextBlock = new()
{
Text = SubItemsCollectionView[i] as string,
};
Grid.SetColumn(subItemTextBlock, 1);
Grid.SetRow(subItemTextBlock, (i * 2) + 1);
Grid.SetRowSpan(subItemTextBlock, 2);
this.RootContainer.Children.Add(subItemTextBlock);
}
}
}
}
private void UpdateRowDefinitions(int mainItemsCount)
{
int requiredRowsCount = mainItemsCount * 2;
while (this.RootContainer.RowDefinitions.Count != requiredRowsCount)
{
if (this.RootContainer.RowDefinitions.Count < requiredRowsCount)
{
this.RootContainer.RowDefinitions.Add(new RowDefinition()
{
Height = new GridLength(0, GridUnitType.Auto),
});
continue;
}
if (this.RootContainer.RowDefinitions.Count > requiredRowsCount)
{
this.RootContainer.RowDefinitions.RemoveAt(this.RootContainer.RowDefinitions.Count - 1);
continue;
}
}
}
}
MainPageViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using System.Collections.ObjectModel;
namespace UserControls;
public partial class MainPageViewModel : ObservableObject
{
[ObservableProperty]
private ObservableCollection<string> mainItems = new()
{
"New York",
"London",
"Edinburgh"
};
[ObservableProperty]
private ObservableCollection<string> subItems = new()
{
"Airplane",
"Train",
};
}
MainPage.xaml
<Grid>
<local:CustomList
MainItems="{x:Bind ViewModel.MainItems, Mode=OneWay}"
SubItems="{x:Bind ViewModel.SubItems, Mode=OneWay}" />
</Grid>
I have a Label and I want to bind Text to a property of an object
public class MainCar: INotifyPropertyChanged
{
string typeCar;
public string TypeCar
{
set
{
if (typeCar != value)
{
typeCar = value;
OnPropertyChanged("TypeCar");
}
}
get
{
return typeCar;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
In my Xaml code I have a label and I do not understand how to bind Text of my Label to object`s property TypeCar
XAML CODE
<Label x:Name="label" FontSize="Large" Text="" />
BEHIND CODE
public Car_add()
{
NavigationPage.SetHasNavigationBar(this, false);
InitializeComponent();
this.BindingContext = new TypesCar();
}
VIEWMODEL CLASS
public class TypesCar
{
public TypesCar()
{
var vm = new MainCar() { TypeCar = "Ford" };
}
this is very well documented
<Label x:Name="label" FontSize="Large" Text="{Binding TypeCar}" />
then in the code behind
var vm = new MainCar() { TypeCar = "Ford" };
this.BindingContext = vm;
OR, if you are binding to a property on the SAME PAGE and NOT A VM
this.BindingContext = this;
note that if you want your UI to update as your VM changes, the VM must implement INotifyPropertyChanged
My binding:
<local:MyContentView BindingContext="{Binding Source={x:Reference Root}, Path=BindingContext.Entity.Recipe, Mode=OneWay}"/>
The BindingContext on the ContentView is being updated when Recipe is changed, but the controls inside MyContentView aren't populating with data. If Recipe is a valid value initially the controls inside MyContentView is populated with the data, but if Recipe starts off as null and is changed to a valid target the controls will not update despite the BindingContext changing.
according to your description, you want to bind contentview in contentpage, the data don't update when data source changed, I guess that you may don't implement INotifypropertychanged for Recipe, you can follow then following article to implement INotifyPropertyChanged.
https://xamarinhelp.com/xamarin-forms-binding/
Another way ti to use Bindableproperty, I do one sample for you, you can take a look:
Contentview:
<ContentView.Content>
<StackLayout>
<Label x:Name="label1" Text="{Binding Text}" />
</StackLayout>
public partial class mycontenview : ContentView
{
public static BindableProperty TextProperty = BindableProperty.Create(
propertyName: "Text",
returnType: typeof(string),
declaringType: typeof(mycontenview),
defaultValue: string.Empty,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: HandlePropertyChanged);
public string Text
{
get
{
return (string)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
}
}
private static void HandlePropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
mycontenview contentview = bindable as mycontenview;
contentview.label1.Text = newValue.ToString();
}
public mycontenview()
{
InitializeComponent();
}
}
MainPage:
<StackLayout>
<Label Text="welcome to xamarin world!"/>
<Button x:Name="btn1" Text="btn1" Clicked="btn1_Clicked"/>
<local:mycontenview Text="{Binding str}"/>
</StackLayout>
public partial class MainPage : ContentPage, INotifyPropertyChanged
{
private string _str;
public string str
{
get { return _str; }
set
{
_str = value;
OnPropertyChanged("str");
}
}
public MainPage()
{
InitializeComponent();
m = new model1() { str = "test 1", str1 = "test another 1" };
str = "cherry";
this.BindingContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs((propertyName)));
}
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(storage, value))
{
return false;
}
storage = value;
OnPropertyChanged(propertyName);
return true;
}
private void btn1_Clicked(object sender, EventArgs e)
{
str = "this is test!";
}
}
In Xamarin.Forms I implemented a custom Picker.
The ItemsSource is set correctly. However when i change the selected item it does not update the property on my ViewModel.
The BindablePicker:
public class BindablePicker : Picker
{
public BindablePicker()
{
this.SelectedIndexChanged += OnSelectedIndexChanged;
}
public static BindableProperty ItemsSourceProperty =
BindableProperty.Create<BindablePicker, IEnumerable>(o => o.ItemsSource, default(IEnumerable), propertyChanged: OnItemsSourceChanged);
public static BindableProperty SelectedItemProperty =
BindableProperty.Create<BindablePicker, object>(o => o.SelectedItem, default(object), propertyChanged: OnSelectedItemChanged);
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public object SelectedItem
{
get { return (object)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
private static void OnItemsSourceChanged(BindableObject bindable, IEnumerable oldvalue, IEnumerable newvalue)
{
var picker = bindable as BindablePicker;
picker.Items.Clear();
if (newvalue != null)
{
//now it works like "subscribe once" but you can improve
foreach (var item in newvalue)
{
picker.Items.Add(item.ToString());
}
}
}
private void OnSelectedIndexChanged(object sender, EventArgs eventArgs)
{
if (SelectedIndex < 0 || SelectedIndex > Items.Count - 1)
{
SelectedItem = null;
}
else
{
SelectedItem = Items[SelectedIndex];
}
}
private static void OnSelectedItemChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var picker = bindable as BindablePicker;
if (newvalue != null)
{
picker.SelectedIndex = picker.Items.IndexOf(newvalue.ToString());
}
}
}
The Xamlpage:
<controls:BindablePicker Title="Category"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
Grid.Row="2"/>
The ViewModel properties, didn't implement the NotifyPropertyChanged on the properties since they only need to be updated from the ´Viewto theViewModel`:
public Category SelectedCategory { get; set; }
public ObservableCollection<Category> Categories { get; set; }
When creating your BindableProperty:
public static BindableProperty SelectedItemProperty =
BindableProperty.Create<BindablePicker, object>(o => o.SelectedItem, default(object), propertyChanged: OnSelectedItemChanged);
without specifying the defaultBindingMode, the BindingMode is set to OneWay, meaning the Binding is updated from source (your view model) to target (your view).
This can be fixed by changing the defaultBindingMode:
public static BindableProperty SelectedItemProperty =
BindableProperty.Create<BindablePicker, object>(o => o.SelectedItem, default(object), BindingMode.TwoWay, propertyChanged: OnSelectedItemChanged);
or, if it's the default you want for your picker, but want to update the source only in this view, you can specify the BindingMode for this instance of the Binding only:
<controls:BindablePicker Title="Category"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
Grid.Row="2"/>
Beside adding the Mode=TwoWay to my binding a had to change some things in my picker so it could work with the actual objects i had it bound to.
The Items property of the Xamarin Picker is an IList<string>
since all my items are added to it as a string it keeps the same indexed value.
Therefor the ItemsSource is changed to an IList:
public IList ItemsSource
{
get { return (IList)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
I also modified the SelectedIndexChangedmethod so it doesn't retrieve the item from the Items but from the ItemsSource, wich is in my case an IList<Category>:
private void OnSelectedIndexChanged(object sender, EventArgs eventArgs)
{
if (SelectedIndex < 0 || SelectedIndex > Items.Count - 1)
{
SelectedItem = null;
}
else
{
SelectedItem = ItemsSource[SelectedIndex];
}
}
In my ViewModel i no longer use the ObservableCollection for my Categories but add these items to an IList<Category>.
The ObservableCollectionhas no use since when my BindablePicker binds to the ItemsSource the items are added to the internal IList<string>. when adding an item to the collection it will not be updated. I now update the entire ItemSourceif an item is changed.
I have the following scenario:
<Button Click="ClickHandler">Click Me</Button>
<TextBox x:Name="MyInputTextBox" />
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox x:Name="MyRepeatTextBox" Text="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
If MyRepeatTextBox.Text == MyInputTextBox.Text I want to change the color of MyRepeatTextBox.Background to Green. If MyRepeatTextBox.Text is blank I want to change the color to red. How would I implement the button click handler?
Not sure a button event would be the best for this.
As DataTriggers are again not brought outside the WPF world, those are out. There's no IMultiValueConverter either. Behaviours are currently out, but there is a nice codeplex project for them. I would use that
public class MatchTextForegroundBehaviour : Behavior<TextBox>
{
private TextBox _attachedElement;
public static readonly DependencyProperty MatchForegroundProperty =
DependencyProperty.Register("MatchForeground", typeof(Brush),
typeof(MatchTextForegroundBehaviour),
new PropertyMetadata(new SolidColorBrush(Colors.Green), OnMatchForegroundChanged));
public static readonly DependencyProperty FallbackForegroundProperty =
DependencyProperty.Register("FallbackForeground", typeof(Brush),
typeof(MatchTextForegroundBehaviour),
new PropertyMetadata(new SolidColorBrush(Colors.Black), OnFallbackForegroundChanged));
public static readonly DependencyProperty TextToMatchProperty =
DependencyProperty.Register("TextToMatch", typeof(string),
typeof(MatchTextForegroundBehaviour),
new PropertyMetadata(null, OnTextToMatchChanged));
public Brush MatchForeground
{
get { return (Brush)GetValue(MatchForegroundProperty); }
set { SetValue(MatchForegroundProperty, value); }
}
public Brush FallbackForeground
{
get { return (Brush)GetValue(FallbackForegroundProperty); }
set { SetValue(FallbackForegroundProperty, value); }
}
public string TextToMatch
{
get { return (string)GetValue(TextToMatchProperty); }
set { SetValue(TextToMatchProperty, value); }
}
/// <summary>
/// Event when the behavior is attached to a element.
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
_attachedElement = AssociatedObject;
if(_attachedElement != null)
_attachedElement.TextChanged += (s,e) => ChangeForeground();
}
private static void OnMatchForegroundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var behavior = (MatchTextForegroundBehaviour)d;
behavior.ChangeForeground();
}
private static void OnTextToMatchChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var behavior = (MatchTextForegroundBehaviour)d;
behavior.ChangeForeground();
}
private static void OnFallbackForegroundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var behavior = (MatchTextForegroundBehaviour)d;
behavior.ChangeForeground();
}
private void ChangeForeground()
{
if (_attachedElement == null) return;
if (string.IsNullOrEmpty(TextToMatch)) return; // change foreground to red?
if (_attachedElement.Text == TextToMatch)
{
_attachedElement.Foreground = MatchForeground;
}
else
{
_attachedElement.Foreground = FallbackForeground;
}
}
}
And the xaml
<TextBox x:Name="MyRepeatTextBox" Text="{Binding}"
TextToMatch="{Binding Text, ElementName=MyInputTextBox}"
FallbackForeground="Black" MatchForeground="Green" />
If a button click event is really how you want to do this, you can try the following. I have not compiled this against WinRT, but I think everything used is in WinRT.
Use the following ExtensionMethod
internal static class TreeExtensions
{
public static T GetChildElement<T>(this DependencyObject element) where T :FrameworkElement
{
if (element == null) return null;
if(element.GetType() == typeof(T)) return (T)element;
T childAsT = null;
int count = VisualTreeHelper.GetChildrenCount(element);
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(element, i);
childAsT = child.GetChildElement<T>();
if (childAsT != null) break;
}
return childAsT;
}
}
Inside the button click event you would do the following (assuming you gave the ItemsControl a name of itemsControl:
foreach (var item in itemsControl.Items)
{
var element = itemsControl.ItemContainerGenerator.ContainerFromItem(item);
var textblock = element.GetChildElement<TextBlock>();
if (textblock != null)
{
if (textblock.Text == MyInputTextBox.Text)
textblock.Foreground = new SolidColorBrush(Colors.Green);
else
textblock.Foreground = new SolidColorBrush(Colors.Black);
}
}