I have a page with a Telerik TabView. This contains two tabs. Each tab contains a view specified as ContentView in separate files. Each view has its own ViewModel, which are referenced in the ViewModel of the page.
In UI I use the sub-ViewModels as BindingContext for elements in the TabHeaders. On initial load the value of the used property is loaded correctly, but when the value is updated the UI doesn't update. Why?
All ViewModels are implementing the same base class which implements the INotifyPropertyChanged and the OnPropertyChanged() method for the used property is called and can be received in the own ViewModel.
Page-ViewModel:
public EquipmentPageViewModel(...){
var locator = (ViewModelLocator)Application.Current.Resources[nameof(ViewModelLocator)];
EquipmentOverviewViewModel = locator.GetViewModel(nameof(IEquipmentOverviewViewModel)) as IEquipmentOverviewViewModel;
CalibrationViewModel = locator.GetViewModel(nameof(ICalibrationViewModel)) as ICalibrationViewModel;
}
public IEquipmentOverviewViewModel EquipmentOverviewViewModel { get; private set; }
public ICalibrationViewModel CalibrationViewModel { get; private set; }
sub-ViewModel (EquipmentOverviewViewModel):
// property bound to an tabHeader in the page
public bool IsEquipmentValid
{
get
{
if (OrderWorkflow != null && OrderWorkflow.IsBatchEditing)
{
if (Operation == null || Operation.Equipment == null)
{
return false;
}
return IsQualityControlValid;
}
else
{
return Operation != null && Operation.Equipment != null && Operation.Equipment.IsEquipmentValid && IsQualityControlValid;
}
}
}
// property bound to an entry element in the view contained in the tab item
public string EquipmentNumber
{
get
{
return Equipment.EquipmentNumber;
}
set
{
if (value != Equipment.EquipmentNumber)
{
Equipment.EquipmentNumber = value;
OnPropertyChanged(nameof(EquipmentNumber));
OnPropertyChanged(nameof(IsEquipmentValid));
}
}
}
Page XAML (sub-ViewModel used in circle:CircleImage):
<ControlTemplate x:Key="EquipmentHeaderTemplate">
<Grid BackgroundColor="Transparent">
<BoxView IsVisible="{TemplateBinding IsSelected}"
BackgroundColor="#007bff"
VerticalOptions="End"
Margin="0, 5, 0, 0"
HeightRequest="3"/>
<StackLayout Orientation="Horizontal"
Margin="15"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand">
<!-- Validation mark -->
problem here---> <circle:CircleImage BindingContext="{Binding EquipmentOverviewViewModel}"
WidthRequest="10"
HeightRequest="10"
BorderColor="Transparent"
BorderThickness="1"
FillColor="#FF3B30"
Aspect="Fill"
IsVisible="{Binding IsEquipmentValid, Converter={StaticResource BooleanInverterConverter}, Mode=TwoWay}"
Margin="5, 5, 5, 0"
HorizontalOptions="Center"
VerticalOptions="Center" />
<iconize:IconLabel Text="ion-ios-construct"
TextColor="#007bff"
VerticalOptions="CenterAndExpand"/>
<Label Text="{Binding [OrderOperationDetailsPage_PivotItemEquipmentHeader], Source={StaticResource LocalizedString}}"
TextColor="#007bff"
Margin="10, 0, 0, 0"
VerticalOptions="CenterAndExpand"/>
</StackLayout>
</Grid>
</ControlTemplate>
...
<primitives:TabViewItem.Header>
<primitives:TabViewHeaderItem ControlTemplate="{StaticResource EquipmentHeaderTemplate}"
IsVisible="{Binding IsCalibrationAllowed}"/>
</primitives:TabViewItem.Header>
<primitives:TabViewItem.Content>
<equipment:EquipmentView></equipment:EquipmentView>
</primitives:TabViewItem.Content>
It seems that you didn't implement the Set method of IsEquipmentValid . So even if you change the value in other lines , it will never been changed .
public bool IsEquipmentValid
{
get
{
if (OrderWorkflow != null && OrderWorkflow.IsBatchEditing)
{
if (Operation == null || Operation.Equipment == null)
{
return false;
}
return IsQualityControlValid;
}
else
{
return Operation != null && Operation.Equipment != null && Operation.Equipment.IsEquipmentValid && IsQualityControlValid;
}
}
set
{
if (value != IsQualityControlValid)
{
value = IsQualityControlValid;
OnPropertyChanged(nameof(IsEquipmentValid));
}
}
}
Related
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);
}
}
}
}
}
In a ViewModel i load data from a FirebaseDatabase and showing it in a CollectionView.
public MainViewModel()
{
var collection = firebase
.Child("Foto/")
.AsObservable<Fotos>()
.Subscribe((dbevent) =>
{
if (dbevent.Object != null)
{
Foto.Add(dbevent.Object);
}
});
}
But i want to change it ToList so i make it Descending based on BalId in the Database.
Want to use GetAllFotosDesending() but i cannot make it work in the MainViewModel.
public async Task<List<Fotos>> GetAllFotosDesending()
{
return (await firebase
.Child("Foto/")
.OnceAsync<Fotos>()).Select(item => new Fotos
{
BalId = item.Object.BalId,
RollNo = item.Object.RollNo,
Foto = item.Object.Foto,
Titel = item.Object.Titel,
Fototekst = item.Object.Fototekst
}).OrderByDescending(x => x.BalId).ToList();
}
2 options , or make it ToList in the MainViewModel or add GetAllFotosDesending() work in the MainViewModel.
The last option is maybe better ? but i cannot make this working when adding to the MainViewModel
This is the CollectionView with ItemsSource="{Binding Foto}"
<CollectionView
x:Name="Dood"
ItemsSource="{Binding Foto}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="Auto, *"
RowDefinitions="Auto, Auto, Auto, 1"
ColumnSpacing="10"
RowSpacing="5"
Padding="0,10">
<Image Source="{Binding Pic}"
Margin="20,0,0,10"
HeightRequest="70"
WidthRequest="70"
HorizontalOptions="Center"
VerticalOptions="Center"
Grid.RowSpan="3"
Grid.Row="0"
Grid.Column="0">
<Image.Clip>
<EllipseGeometry
Center="35,35"
RadiusX="35"
RadiusY="35"/>
</Image.Clip>
</Image>
<Label Text="{Binding Titel}"
FontAttributes="Bold"
Grid.Column="1"
Grid.Row="0"/>
<Label Text="{Binding Email}"
Grid.Column="1"
Grid.Row="1"/>
<Label Text="{Binding RollNo}"
Grid.Column="1"
Grid.Row="2"/>
<BoxView Style="{StaticResource SeparatorLine}"
Grid.Column="0"
Grid.Row="3"
Grid.ColumnSpan="2"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Binding like this
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
OrderBy does not have Alphanumeric comparison by default, the default Orderby only compares Alphabets or Numbers not a combination of both to handle this you need to create your own Alphanumeric Comparator that does this for you (especially if these are string values)
You can use this basic comparator for both Ascending and Descending orders:
public class AlphanumComparator : IComparer<object>
{
private enum ChunkType { Alphanumeric, Numeric };
private bool InChunk(char ch, char otherCh)
{
ChunkType type = ChunkType.Alphanumeric;
if (char.IsDigit(otherCh))
{
type = ChunkType.Numeric;
}
return (type != ChunkType.Alphanumeric || !char.IsDigit(ch))
&& (type != ChunkType.Numeric || char.IsDigit(ch));
}
public int Compare(object x, object y)
{
string firstString = x as string;
string secondString = y as string;
if (string.IsNullOrWhiteSpace(firstString) || string.IsNullOrWhiteSpace(secondString))
{
return 0;
}
int firstMarker = 0, secondMarker = 0;
while ((firstMarker < firstString.Length) || (secondMarker < secondString.Length))
{
if (firstMarker >= firstString.Length)
{
return -1;
}
else if (secondMarker >= secondString.Length)
{
return 1;
}
char firstCh = firstString[firstMarker];
char secondCh = secondString[secondMarker];
StringBuilder thisChunk = new StringBuilder();
StringBuilder thatChunk = new StringBuilder();
while ((firstMarker < firstString.Length) && (thisChunk.Length == 0 || InChunk(firstCh, thisChunk[0])))
{
thisChunk.Append(firstCh);
firstMarker++;
if (firstMarker < firstString.Length)
{
firstCh = firstString[firstMarker];
}
}
while ((secondMarker < secondString.Length) && (thatChunk.Length == 0 || InChunk(secondCh, thatChunk[0])))
{
thatChunk.Append(secondCh);
secondMarker++;
if (secondMarker < secondString.Length)
{
secondCh = secondString[secondMarker];
}
}
int result = 0;
// If both chunks contain numeric characters, sort them numerically
if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0]))
{
int thisNumericChunk = Convert.ToInt32(thisChunk.ToString());
int thatNumericChunk = Convert.ToInt32(thatChunk.ToString());
if (thisNumericChunk < thatNumericChunk)
{
result = -1;
}
if (thisNumericChunk > thatNumericChunk)
{
result = 1;
}
}
else
{
result = thisChunk.ToString().CompareTo(thatChunk.ToString());
}
if (result != 0)
{
return result;
}
}
return 0;
}
}
Once you are done, you use it like below:
OrderByDescending(x => x.BalId, new AlphanumComparator());
OrderBy(x => x.BalId, new AlphanumComparator());
Solved like it this.
public async Task<List<Fotos>> GetAllFotosDesending()
{
return (await firebase
.Child("Foto/")
.OnceAsync<Fotos>()).Select(item => new Fotos
{
BalId = item.Object.BalId,
RollNo = item.Object.RollNo,
Foto = item.Object.Foto,
Titel = item.Object.Titel,
Fototekst = item.Object.Fototekst
}).OrderByDescending(x => x.BalId).ToList();
}
public async void InitializeAsync()
{
Fotos = await GetAllFotosDesending();
}
public MainViewModel()
{
InitializeAsync();
}
I am using a DisplayAlert like below in my project.
var answer = await DisplayAlert("Alert", "You are invited to join a group, would you like to accept it or not?", "Accept", "Reject");
if (answer)
{
//accept invitation
}
else
{
//reject invitation
}
Accept and Reject options are working fine. My problem is Reject option is executing when tapping on the background or device back arrow. Is it has a simple solution other than implementing Rg.Plugins.Popup?
I had a similar request once, and I quickly "solved it" with a workaround like this (using DisplayActionSheet):
bool isActionSelected = false;
while(!isActionSelected)
{
string action = await DisplayActionSheet ("You are invited to join a group, would you like to accept it or not?", null, null, "Accept", "Reject");
if (action == "Accept")
{
isActionSelected = true;
//do stuff
}
else if (action == "Reject")
{
isActionSelected = true;
//do stuff
}
else
{
isActionSelected = false;
}
}
This is not suggested, unless you are in a hurry.
So, I would suggest you creating a custom popupView, something like this
<ContentView x:Name="popupView" BackgroundColor="#C0808080" Padding="10, 0" IsVisible="false" AbsoluteLayout.LayoutBounds="0, 0, 1, 1" AbsoluteLayout.LayoutFlags="All">
<StackLayout VerticalOptions="Center" HorizontalOptions="Center">
<StackLayout Orientation="Vertical" HeightRequest="150" WidthRequest="200" BackgroundColor="White">
<Label x:Name="myLabel" TextColor="Black" Text="You are invited to join a group, would you like to accept it or not?" />
<Button Text="Accept" TextTransform="None" Clicked="AcceptClicked" />
<Button Text="Reject" TextTransform="None" Clicked="RejectClicked" />
</StackLayout>
</StackLayout>
</ContentView>
then in .cs
popupView.IsVisible = true;
when you want this to appear.
The only solution - Use DependencyService to implement the alert on each platform .
Interface in Forms
public interface IShowAlertService
{
Task<bool> ShowAlert(string title, string message, string ok, string cancel);
}
Android Implementation
[assembly: Dependency(typeof(ShowAlertService))]
namespace XamarinTableView.Droid
{
class ShowAlertService : IShowAlertService
{
TaskCompletionSource<bool> taskCompletionSource;
public Task<bool> ShowAlert(string title, string message, string ok, string cancel)
{
taskCompletionSource = new TaskCompletionSource<bool>();
Android.App.AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.Instance);
AlertDialog alert = dialog.Create();
alert.SetTitle("Title");
alert.SetMessage("Complex Alert");
alert.SetButton("OK", (c, ev) =>
{
// Ok button click task
Console.WriteLine("Okay was clicked");
taskCompletionSource.SetResult(true);
});
alert.SetButton2("CANCEL", (c, ev) => {
Console.WriteLine("Cancel was clicked");
taskCompletionSource.SetResult(false);
});
alert.Show();
return taskCompletionSource.Task;
}
}
}
iOS Implementation
[assembly: Dependency(typeof(ShowAlertService))]
namespace XamarinTableView.iOS
{
class ShowAlertService : IShowAlertService
{
TaskCompletionSource<bool> taskCompletionSource;
public Task<bool> ShowAlert(string title, string message, string ok, string cancel)
{
taskCompletionSource = new TaskCompletionSource<bool>();
var okCancelAlertController = UIAlertController.Create(title, message, UIAlertControllerStyle.Alert);
//Add Actions
okCancelAlertController.AddAction(UIAlertAction.Create(ok, UIAlertActionStyle.Default, alert => {
Console.WriteLine("Okay was clicked");
taskCompletionSource.SetResult(true);
}));
okCancelAlertController.AddAction(UIAlertAction.Create(cancel, UIAlertActionStyle.Cancel, alert => {
Console.WriteLine("Cancel was clicked");
taskCompletionSource.SetResult(false);
}));
UIWindow window = UIApplication.SharedApplication.KeyWindow;
var viewController = window.RootViewController;
//Present Alert
viewController.PresentViewController(okCancelAlertController, true, null);
return taskCompletionSource.Task;
}
}
Consume in Forms
bool isOk = await DependencyService.Get<IShowAlertService>().ShowAlert("Alert", "You have been alerted", "OK", "Cancel");
if (isOk)
{
}
else
{
}
In this way clicking outside the alert will not trigger cancel event .
Refer to DisplayAlert Xamarim forms.
I need a help..
I need to get data from database using Controller.. But nothing happens when I do it.
My api is php codeigniter and i'm using X-API-Key.. I want to use It in MVC structure, because I need my view be navigable.
HttpClient
public class HttpClient
{
private Dictionary<string, string> _headers;
private HttpClient restClient;
public HttpClient(Dictionary<string, string> headers)
{
if (headers != null) _headers = headers;
else _headers = new Dictionary<string, string>();
}
public string this[string Key]
{
get
{
return _headers[Key];
}
set
{
_headers[Key] = value;
}
}
///<Summary>
/// HTTP GET REQUEST
///</Summary>
public async Task<HttpResponse<T>> ExcecuteAsync<T>(Method method, string baseUrl, Dictionary<string, object> formdata = null)
{
var client = new RestClient(baseUrl);
var request = new RestRequest(method);
foreach(var header in _headers)
{
request.AddHeader(header.Key, header.Value);
}
if(formdata != null)
{
foreach(var item in formdata)
{
request.AddParameter(item.Key, item.Value);
}
}
IRestResponse response = null;
Exception ex = null;
try
{
response = await client.ExecuteTaskAsync(request);
}
catch(Exception _ex)
{
ex = _ex;
}
var httpresponse = new HttpResponse<T>
{
Response = response
};
if (response == null)
{
httpresponse.Status = new StatusResponse
{
code = -1,
message = $"Nao se pode obter resposta do servidor, stacktrace:{ex.StackTrace}"
};
return httpresponse;
}
var jsonresult = response.Content;
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
var telement = JsonConvert.DeserializeObject<T>(jsonresult);
httpresponse.Result = telement;
}else if(response.StatusCode == 0)
{
httpresponse.Status = new StatusResponse
{
code = 0,
message = $"Nao se pode obter resposta do servidor"
};
}
else
{
var telement = JsonConvert.DeserializeObject<StatusResponse>(jsonresult);
httpresponse.Status = telement;
}
return httpresponse; //null
}
This is my App.xaml - where i have my x-api-key
App.RestClient = new HttpClient(new Dictionary<string, string>
{
{"X-API-Key", "926ff4c0-d86e-421f-9192-82ac52650f5c" }
});
App.BaseUrl = "http://10.156.96.80/apiuthomi";
protected override async void OnStart()
{
var ListasHospitais = await new HospitalService(App.RestClient).all();
MainPage = new ListHospital(ListasHospitais);
}
This is ListHospital.xaml
<StackLayout>
<ListView
HasUnevenRows="True"
SeparatorVisibility="Default"
x:Name="ListasHospitais"
IsPullToRefreshEnabled="True"
Margin="10">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid Padding="10" RowSpacing="10" ColumnSpacing="10" BackgroundColor="White" Margin="0 , 0 , 0, 10">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="1"
VerticalOptions="End"
Text="{Binding Nome}"
MaxLines="1"
LineBreakMode="TailTruncation"
FontAttributes="Bold"
/>
<Label Grid.Column="1"
Grid.Row="1"
Text="{Binding Detalhes}"
VerticalOptions="Start"
LineBreakMode="TailTruncation"
MaxLines="2"
/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
This is my ListHospital.xaml.cs
public partial class ListHospital : ContentPage
{
public ObservableCollection<Cliente> _clientes;
public ListHospital(List<Cliente>clientes)
{
InitializeComponent();
NavigationPage.SetHasNavigationBar(this, false);
_clientes = new ObservableCollection<Cliente>();
ListasHospitais.ItemsSource = _clientes;
foreach(var cliente in clientes)
{
_clientes.Add(cliente);
}
}
public void voltar_Click(object sender, System.EventArgs e)
{
Navigation.PopAsync();
}
}
HospitalService = where i will write all methods
public class HospitalService
{
private HttpClient _restclient;
public HospitalService(HttpClient restclient)
{
if (restclient != null) _restclient = restclient;
else throw new NullReferenceException("http nao pode ser nulo");
}
public async Task<List<Cliente>>all()
{
var response = await _restclient.GetAsync<List<Cliente>>("http://10.156.96.80/apiuthomi/cliente/all");
if (response.Result != null) return response.Result;
return new List<Cliente>();
}
}
until here everything is going well and it's working.. but the problem is that my view listHospital os no longer clickable.. like I want tho make this page be navigable...
this public ListHospital(List<Cliente>clientes)does not allow the clicked method to be valid in my view
I can't do await Navigation.PushAsync(new ListHospital()) , It give
me an error
this is because your ListHosptial page does not have a default (empty) constructor and you are not passing the required data. You can fix this by adding an default constructor
public ListHospital()
{
InitializeComponent();
NavigationPage.SetHasNavigationBar(this, false);
...
}
if you do this then ListHospital needs to load it's own data. The easiest way to do this is probably to add a call to your services in the OnAppearing method
public override async void OnAppearing()
{
// call services to load data and assign ItemsSource
var data = await new HospitalService(App.RestClient).all();
ListasHospitais.ItemsSource = data;
}
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;
}