How to reference and bind a property at the same time? - xamarin.forms

Here is what I mean by the title. I have an Entry with a property called "IsValid" (bool) which is bound to a behaviour (it is called Validator) which checks if the input is in range (0 - 10 in this case) and colour the background of the entry red or transparent. That works fine. However, as I have the same logic in my ViewModel (to check if the input is in range) and show a dialog message if it is not, I wanted to directly bind to the validator's IsValid and use the bind fild in my viewModel (IsBinReferenceValid) abd thus remove the locig from the vm. Currently, the property in my vm IsBinReferenceValid is not changed in any way which indicates that the binding does not work.
Here is the xaml code:
<userControl:DetailedEntry
PlaceholderLabel="{x:Static locale:BinPrintLang.BinRef}"
Text="{Binding BinTextEntry}"
TextColor="{StaticResource PrimaryColor}"
BgColor="White"
BorderColor="{StaticResource DisableColor}"
VerticalOptions="CenterAndExpand"
IsLabelVisible="True"
Label="Bin Reference"
IsImportant="True"
IsValid="{Binding Source={x:Reference InputLengthValidator}, Path=IsValid}">
<userControl:DetailedEntry.EntryBehavior>
<ui:InputLengthValidator x:Name="InputLengthValidator"
MinValue="0"
MaxValue="10"
IsValid="{Binding Source=IsBinReferenceValid, Mode=OneWayToSource}"/>
</userControl:DetailedEntry.EntryBehavior>
</userControl:DetailedEntry>
Any ideas how I can reference and bind to a property at the same time, is that even possible (That is, if that is where the problem is coming from)?
Base validator code:
public class ValueInRangeValidator : Validator<Entry>
{
private static BindableProperty MinValueProperty =
BindableProperty.Create("MinValue", typeof(decimal?), typeof(ValueInRangeValidator));
public decimal? MinValue
{
get { return (decimal?) GetValue(MinValueProperty); }
set
{
SetValue(MinValueProperty, value);
OnPropertyChanged();
}
}
public static BindableProperty MaxValueProperty =
BindableProperty.Create("MaxValue", typeof(decimal?), typeof(ValueInRangeValidator));
public decimal? MaxValue
{
get { return (decimal?) GetValue(MaxValueProperty); }
set
{
SetValue(MaxValueProperty, value);
OnPropertyChanged();
}
}
public virtual void Bindable_TextChanged(object sender, TextChangedEventArgs e)
{
decimal i = 0;
IsValid = decimal.TryParse(e.NewTextValue, out i);
IsValid = IsValid && (MinValue == null ? i >= decimal.MinValue : i >= MinValue);
IsValid = IsValid && (MaxValue == null ? i <= decimal.MaxValue : i <= MaxValue);
}
protected override void OnAttachedTo(Entry bindable)
{
bindable.TextChanged += Bindable_TextChanged;
}
protected override void OnDetachingFrom(Entry bindable)
{
bindable.TextChanged -= Bindable_TextChanged;
}
}
InputLengthValidator code:
public class InputLengthValidator : ValueInRangeValidator
{
public override void Bindable_TextChanged(object sender, TextChangedEventArgs e)
{
var max = (int) MaxValue;
var min = (int) MinValue;
var textLenght = e.NewTextValue.Length;
IsValid = textLenght >= min && textLenght < max;
}
}

I managed to get the validation working by subscribing to another (custom) bindable property of my DetailedEntry control called IsWarning.
<userControl:DetailedEntry
rid.Row="1"
Grid.Column="0"
Label="{x:Static locale:GoodsReceiptLang.NumLabels}"
Text="{Binding NumberOfLabels, Mode=TwoWay}"
TextColor="{StaticResource PrimaryColor}"
Keyboard="Numeric"
IsImportant="True"
IsWarning="{Binding ShowWarning}">
</userControl:DetailedEntry>
My VM:
private bool CanPrint()
{
var errors = new List<string>();
ShowWarning = false;
if (SelectedPrinter == null)
errors.Add(CommonLang.SelectPrinterErrorMsg);
if (string.IsNullOrEmpty(NumberOfLabels) || !int.TryParse(NumberOfLabels, out int numLabels))
{
ShowWarning = true;
errors.Add(CommonLang.NotValidInput);
}
if (errors.Any())
{
ShowErrorMessage(string.Join(" ", errors.ToArray()));
return false;
}
return true;
}

Related

ImageSource Binding not showing

I have already tried to solve it via reddit but i did not find any solution there. I have following code which gets me and ImageSource from my AccountViewModel and sets the ProfileImage property on the CommentViewModel to the same Image. The Image is not available as a saved files since it comes from my Backend Server. I already chacked, that the ProfileImage Property actually gets the right image, it does, also the Property Changed event fires and if i set the Property to an ImageSource.FromFile with a test Image which i have saved as a file it works. I really don't see any reason why it would not work with the image i get from the other viewmodel.
As the Image is a normal Property mit [BindableProperty] Annotation in the AccountViewModel and i have confirmed that the Types are exactly the same i do not show the AccoutnViewModel here to make it a bit shorter.
I do not get why the Start of every CodeBlock looks so strange.
CommentView:
<ContentPage.Content>
<StackLayout Margin="15">
<StackLayout Orientation="Horizontal">
<ffimageloading:CachedImage Source="{Binding ProfileImage, FallbackValue=default_user.jpg}"
HeightRequest="50"
WidthRequest="50"
VerticalOptions="CenterAndExpand"
HorizontalOptions="StartAndExpand">
<ffimageloading:CachedImage.Transformations>
<fftransformations:CircleTransformation/>
</ffimageloading:CachedImage.Transformations>
</ffimageloading:CachedImage>
<Label Text="{Binding Username}"
FontSize="13"/>
</StackLayout>
</StackLayout>
</ContentPage.Content>
CommentViewModel:
public partial class CommentViewModel : BaseViewModel
{
// == constants ==
// == observable properties ==
[ObservableProperty]
public long id;
[ObservableProperty]
public string username;
[ObservableProperty]
public string description;
[ObservableProperty]
ImageSource profileImage;
partial void OnProfileImageChanged(ImageSource value)
{
Console.WriteLine("profile changed");
}
// == constructors ==
public CommentViewModel(DisplayPostViewModel displayPostViewModel)
{
//profileImage = ImageSource.FromFile("default_user.jpg");
navigationService.DataBetweenViewModel<AccountViewModel>(this, "ProfileImage", "ProfileImage", true);
//profileImage = ImageSource.FromFile("default_user.jpg");
}
public CommentViewModel()
{
}
}
NavigationService:
public bool DataBetweenViewModel<ReceivingViewModel>(BaseViewModel sendingViewModel, string sendingPropertyName = null, string receivingPropertyName = null, bool isGettingFromOther = false)
where ReceivingViewModel : BaseViewModel
{
try
{
PropertyTransferObject transferObject;
var mainpage = Application.Current.MainPage as NavigationPage;
var tabbedPage = mainpage.RootPage as TabbedPage;
var recievingVM = tabbedPage.Children.SelectMany(tab => tab.Navigation.NavigationStack?
.Select(page => page.BindingContext)).OfType<ReceivingViewModel>();
if (isGettingFromOther)
{
transferObject = new PropertyTransferObject(recievingVM.First(), sendingViewModel, sendingPropertyName, receivingPropertyName);
}
else
{
transferObject = new PropertyTransferObject(sendingViewModel, recievingVM.First(), sendingPropertyName, receivingPropertyName);
}
objectMapper.TransferProperties(transferObject);
return true;
}
catch( Exception ex)
{
string e = ex.ToString();
return false;
}
}
ObjectMapper:
public void TransferProperties(PropertyTransferObject propertyTransferObject)
{
if (propertyTransferObject.SendingPropertyName != null && propertyTransferObject.ReceivingPropertyName != null
|| (propertyTransferObject.SendingPropertyName != String.Empty && propertyTransferObject.ReceivingPropertyName != String.Empty))
{
foreach (PropertyInfo recievingProp in propertyTransferObject.ReceivingObject.GetType().GetProperties())
{
foreach (PropertyInfo sendingProp in propertyTransferObject.SendingObject.GetType().GetProperties())
{
if (sendingProp.Name == propertyTransferObject.SendingPropertyName && recievingProp.Name == propertyTransferObject.ReceivingPropertyName)
{
recievingProp.SetValue(propertyTransferObject.ReceivingObject, sendingProp.GetValue(propertyTransferObject.SendingObject, null), null);
}
}
}
}
if (propertyTransferObject.SendingPropertyName == null && propertyTransferObject.ReceivingPropertyName == null
|| (propertyTransferObject.SendingPropertyName == String.Empty && propertyTransferObject.ReceivingPropertyName == String.Empty))
{
foreach (PropertyInfo recievingProp in propertyTransferObject.ReceivingObject.GetType().GetProperties())
{
foreach (PropertyInfo sendingProp in propertyTransferObject.SendingObject.GetType().GetProperties())
{
if (recievingProp.Name == sendingProp.Name && recievingProp.PropertyType == sendingProp.PropertyType)
{
recievingProp.SetValue(propertyTransferObject.ReceivingObject, sendingProp.GetValue(propertyTransferObject.SendingObject, null), null);
}
}
}
}
}

Xaramin form -calling variable from other .cs file

I am doing a quiz game in Xaramin. forms. and for the score function. if the user got a correct answer, I want the score will add 1.but in my case even the give the correct answer, the score is not adding.
I am also trying to bind to the "score" variable to a label. I want to know if i put a correct code or not.
Button
private void submit_Clicked(object sender, EventArgs e)
{
string answer = this.answer.Text;
string canswer = "correct";
if (answer != null)
{
string ranswer = answer.Replace(" ", string.Empty);
if (ranswer.ToLower() == canswer)
{
DisplayAlert("GoodJob", "You got the correct answer", "OK");
bindingModel b = new bindingModel();
b.score++;
(sender as Button).IsEnabled = false;
}
else
{
DisplayAlert("Unfortunately", "Your answer is wrong", "OK");
(sender as Button).IsEnabled = false;
}
}
}
ViewModel
public class bindingModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public int displayScore => Score;
public int score = 0;
void OnPropertyChanged(int score)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(score.ToString()));
}
public int Score
{
get => score;
set
{
if (score != value)
{
score = value;
OnPropertyChanged(score);
}
}
}
}
Model
<Label Text="{Binding Score}"/>
in your page constructor, keep a reference to your VM
bindingModel VM;
// this is your constructor, the name will match your page name
public MyPage()
{
InitializeComponent();
this.BindingContext = VM = new bindingModel();
...
}
then in your event handler, you do NOT need to create a new bindingModel
// update the Count on the VM
VM.Count++;
Answer
There's two things broken here:
You are re-initializing your ViewModel instead of referencing the same instance
You are passing the wrong value into PropertyChangedEventArgs
1. Referencing the View Model
You are re-initializing the ViewModel every time by calling bindingModel b = new bindingModel();
Lets initialize the ViewModel once, store it as a field, set it as the BindingContext for our ContentPage, and reference that field in submit_Clicked
public partial class QuizPage : ContentPage
{
readonly bindingModel _bindingModel;
public QuizPage()
{
_bindingModel = new bindingModel();
BindingContext = _bindingModel;
}
private async void submit_Clicked(object sender, EventArgs e)
{
string answer = this.answer.Text;
string canswer = "correct";
Button button = (Button)sender;
if (answer != null)
{
string ranswer = answer.Replace(" ", string.Empty);
if (ranswer.ToLower() == canswer)
{
await DisplayAlert("GoodJob", "You got the correct answer", "OK");
_bindingModel.score++;
button.IsEnabled = false;
}
else
{
await DisplayAlert("Unfortunately", "Your answer is wrong", "OK");
button.IsEnabled = false;
}
}
}
}
2. PropertyChangedEventArgs
You need to pass in the name of the property to PropertyChangedEventArgs.
They way PropertyChanged works is that it announces the name of the property that has changed. In this case, it needs to broadcast that the Score property has changed.
Let's use nameof(Score) to pass in the string "Score" to PropertyChangedEventArgs:
void OnScorePropertyChanged()
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(displayScore)));
}
public int Score
{
get => score;
set
{
if (score != value)
{
score = value;
OnScorePropertyChanged();
}
}
}

Xamarin forms limit number of decimal places in an entry field

I have an entry field inside of a collection view cell. The entry field should accept numeric value only with certain decimal places. I don't want more input if they exceed the decimal places. The decimal places vary for each cell. How do I create this validation in Model or using behaviour/trigger?
Currently, my code is like this
<Entry Placeholder="Quantity"
Keyboard="Numeric"
Text="{Binding EnteredQuantity, Mode=TwoWay}"/>
And in the model part, I am doing this
private string enteredQuantity = "1";
public string EnteredQuantity
{
get => enteredQuantity;
set
{
if (!decimal.TryParse(value, out decimal parsedQuantity))
{
enteredQuantity = "1";
OnPropertyChanged(enteredQuantity);
}
else
{
if (parsedQuantity > 9999.99M)
{
enteredQuantity =string.Format("9999.99", parsedQuantity);
OnPropertyChanged(enteredQuantity);
}
else
{
string formatString = "{" + "0:0.".PadRight(4 + (int) BaseDecimalPlaces, '#') + "}"; //Format of {0:0.##}
if (IsAlternateUnitUsed && selectedUnit.Value == "A")
{
formatString = "{" + "0:0.".PadRight(4 + (int) AlternateDecimalPlaces, '#') + "}";
}
enteredQuantity = string.Format(formatString, parsedQuantity);
OnPropertyChanged(enteredQuantity);
}
}
}
}
For the above test, the format string is {0:0.##} and decimal places are 2.
I am trying to restrict the value in the Binding property. However, this does not reflect on the front end. The frontend will show any decimal places like in the picture below. Does anybody have an idea how to solve this?
Has
Use behavior to achieve this
Xaml Code
...
xmlns:local="clr-namespace:DummyTestApp"
...
<Entry HorizontalOptions="FillAndExpand" VerticalOptions="CenterAndExpand">
<Entry.Behaviors>
<local:LengthValidateBehavior MaxLength="7"/>
</Entry.Behaviors>
</Entry>
Behavior Class
public class LengthValidateBehavior : Behavior<Entry>
{
public static BindableProperty MaxLengthProperty = BindableProperty.Create(nameof(MaxLength), typeof(int), typeof(LengthValidateBehavior), 5/* default value*/);
public int MaxLength
{
get
{
return (int)GetValue(MaxLengthProperty);
}
set
{
SetValue(MaxLengthProperty, value);
}
}
protected override void OnAttachedTo(Entry entry)
{
entry.TextChanged += OnEntryTextChanged;
base.OnAttachedTo(entry);
}
protected override void OnDetachingFrom(Entry entry)
{
entry.TextChanged -= OnEntryTextChanged;
base.OnDetachingFrom(entry);
}
void OnEntryTextChanged(object sender, TextChangedEventArgs args)
{
if (sender is Entry entry)
{
if (args.NewTextValue.Length > MaxLength)// write your logic here
{
entry.Text = args.OldTextValue;
}
}
}
}
Similarly use your own logic to restrict the entry as per your own requirements.

Initializing Binded int Property Value in Caliburn does not map it to mahApps SplitButton SelectedIndex Property

I'm initializing the value like this: (MahApps.Metro 1.1.3-ALPHA)
private int currentCulture;
public int CurrentCulture
{
get { return currentCulture; }
set
{
if (currentCulture != value)
{
currentCulture = value;
LocalizeDictionary.Instance.Culture = Languages[value];
NotifyOfPropertyChange(() => CurrentCulture);
}
}
}
protected override void OnInitialize()
{
base.OnInitialize();
Languages.AddRange(BS.Expert.Client.App.Utils.Resources.GetAvailableCultures());
CurrentCulture = Languages.IndexOf(new CultureInfo(LocalizeDictionary.Instance.Culture.Name));
...
The thing is that I'm not seeing the value set in the dropDown mahApps control when the view is launched..., what's going on?
<Controls:SplitButton
Orientation="Horizontal"
x:Name="Languages"
cal:Message.Attach="[Event SelectionChanged] = [Action ChangeCulture($eventArgs)]"
DisplayMemberPath="DisplayName"
SelectedIndex="{Binding CurrentCulture, ElementName=SplitButton0, Mode=TwoWay}" >
</Controls:SplitButton>

Save selected items when using caliburn.micro /Telerik RadGridView /Silverlight

I am using Caliburn micro(1.3)/MVVM and Silverlight. When I update the itemsource RadGridView, I lose the selected items. I found a blog about implementing a behavior to save the selected items when you are implementing MVVM. I can get the selected items, but I cannot set them back once the itemsource is refreshed. Can someoneshow me how to implement this using caliburn.micro and the RadGridVIew? I think the best way to go is to create a caliburn micro convention, but I can only find a reference for creating a convention for selectedItem, not selectedItems.
Can someone show me how to accomplish this? I tried the following, but it does not work.
private static void SetRadGridSelecteditemsConventions()
{
ConventionManager
.AddElementConvention<DataControl>(DataControl.ItemsSourceProperty, "SelectedItem", "SelectionChanged")
.ApplyBinding = (viewModelType, path, property, element, convention) =>
{
ConventionManager.SetBinding(viewModelType, path, property, element, convention, DataControl.ItemsSourceProperty);
if (ConventionManager.HasBinding(element, DataControl.SelectedItemProperty))
return true;
var index = path.LastIndexOf('.');
index = index == -1 ? 0 : index + 1;
var baseName = path.Substring(index);
foreach (var selectionPath in
from potentialName in ConventionManager.DerivePotentialSelectionNames(baseName)
where viewModelType.GetProperty(potentialName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) != null
select path.Replace(baseName, potentialName))
{
var binding = new Binding(selectionPath) { Mode = BindingMode.TwoWay };
BindingOperations.SetBinding(element, DataControl.SelectedItemProperty, binding);
}
return true;
};
}
Thanks,
Stephane
You should use a behavior for this since the SelectedItems property is readonly.
Telerik has an example for this, only the example is not specific for caliburn.micro.
If you add the following class to your project:
public class MultiSelectBehavior : Behavior<RadGridView>
{
public INotifyCollectionChanged SelectedItems
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(INotifyCollectionChanged), typeof(MultiSelectBehavior), new PropertyMetadata(OnSelectedItemsPropertyChanged));
private static void OnSelectedItemsPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
var collection = args.NewValue as INotifyCollectionChanged;
if (collection != null)
{
collection.CollectionChanged += ((MultiSelectBehavior)target).ContextSelectedItems_CollectionChanged;
}
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItems.CollectionChanged += GridSelectedItems_CollectionChanged;
}
void ContextSelectedItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
UnsubscribeFromEvents();
Transfer(SelectedItems as IList, AssociatedObject.SelectedItems);
SubscribeToEvents();
}
void GridSelectedItems_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UnsubscribeFromEvents();
Transfer(AssociatedObject.SelectedItems, SelectedItems as IList);
SubscribeToEvents();
}
private void SubscribeToEvents()
{
AssociatedObject.SelectedItems.CollectionChanged += GridSelectedItems_CollectionChanged;
if (SelectedItems != null)
{
SelectedItems.CollectionChanged += ContextSelectedItems_CollectionChanged;
}
}
private void UnsubscribeFromEvents()
{
AssociatedObject.SelectedItems.CollectionChanged -= GridSelectedItems_CollectionChanged;
if (SelectedItems != null)
{
SelectedItems.CollectionChanged -= ContextSelectedItems_CollectionChanged;
}
}
public static void Transfer(IList source, IList target)
{
if (source == null || target == null)
return;
target.Clear();
foreach (var o in source)
{
target.Add(o);
}
}
}
This behavior takes care of the synchronization between collection RadGridView.SelectedItems and MultiSelectBehavior.SelectedItems.
Now we need to have an ObservableCollection in the ViewModel
//Collection holding the selected items
private ObservableCollection<object> selectedGridItems;
public ObservableCollection<object> SelectedGridItems
{
get
{
if (selectedGridItems == null)
selectedGridItems = new ObservableCollection<object>();
return selectedGridItems;
}
set
{
if (selectedGridItems == value) return;
selectedGridItems = value;
NotifyOfPropertyChange(() => SelectedGridItems);
}
}
//Deselect all selected items in the gridview
public void ClearSelectedGridItems()
{
SelectedGridItems.Clear();
}
Last thing is bind the behavior in the view
<telerik:RadGridView x:Name="CustomLogs" AutoGenerateColumns="true" SelectionMode="Extended">
<i:Interaction.Behaviors>
<local:MultiSelectBehavior SelectedItems="{Binding SelectedGridItems}"/>
</i:Interaction.Behaviors>
</telerik:RadGridView>
Thats it, hope it helps you!

Resources