I've been trying to reproduce the simplest validation logic ever possible with template 10 validation, but I Just dont get it.
I have created my Model like the wiki at the example at github:
public class User : ValidatableModelBase
{
//public User()
//{
// FirstName = string.Empty;
// LastName = string.Empty;
// Validator = i =>
// {
// var u = i as User;
// if (string.IsNullOrEmpty(u.FirstName))
// u.Properties[nameof(u.FirstName)].Errors.Add("The first name is required");
// else if (u.FirstName.Length <= 3)
// u.Properties[nameof(u.FirstName)].Errors.Add("First Name must be greater than 3 chars.");
// if (string.IsNullOrEmpty(u.LastName))
// u.Properties[nameof(u.LastName)].Errors.Add("The last name is required");
// else if (u.LastName.Length <= 3)
// u.Properties[nameof(u.LastName)].Errors.Add("Last Name must be greater than 3 chars.");
// };
//}
public int Id { get; set; }
public string FirstName
{
get
{
return Read<string>();
}
set
{
Write(value);
}
}
public string LastName
{
get
{
return Read<string>();
}
set
{
Write(value);
}
}
public override string ToString() => $"{FirstName} {LastName}";
}
as you can see I've even created a constructor to initialize my Object, I commented out because I wanted to initialize the values at my ViewModel Constructor.
Then my view model is this:
public class MainPageViewModel : ViewModelBase
{
public MainPageViewModel()
{
User = new User
{
FirstName = string.Empty,
LastName = string.Empty,
Validator = i =>
{
var u = i as User;
if (string.IsNullOrEmpty(u.FirstName))
u.Properties[nameof(u.FirstName)].Errors.Add("The first name is required");
else if (u.FirstName.Length <= 3)
u.Properties[nameof(u.FirstName)].Errors.Add("First Name must be greater than 3 chars.");
if (string.IsNullOrEmpty(u.LastName))
u.Properties[nameof(u.LastName)].Errors.Add("The last name is required");
else if (u.LastName.Length <= 3)
u.Properties[nameof(u.LastName)].Errors.Add("Last Name must be greater than 3 chars.");
},
};
}
private User _User;
public User User
{
get { return _User; }
set { _User = value; }
}
public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> suspensionState)
{
User.Validate();
await Task.CompletedTask;
}
public override async Task OnNavigatingFromAsync(NavigatingEventArgs args)
{
args.Cancel = false;
await Task.CompletedTask;
}
public void GotoSettings() =>
NavigationService.Navigate(typeof(Views.SettingsPage), 0);
public void GotoPrivacy() =>
NavigationService.Navigate(typeof(Views.SettingsPage), 1);
public void GotoAbout() =>
NavigationService.Navigate(typeof(Views.SettingsPage), 2);
}
Now at my view all I have is this:
<Page x:Class="ValidationSample.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Behaviors="using:Template10.Behaviors"
xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:controls="using:Template10.Controls"
xmlns:validate="using:Template10.Controls.Validation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:ValidationSample.Views"
xmlns:m="using:ValidationSample.Models"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:ValidationSample.ViewModels" mc:Ignorable="d">
<Page.DataContext>
<vm:MainPageViewModel x:Name="ViewModel" />
</Page.DataContext>
<RelativePanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="AdaptiveVisualStateGroup">
<VisualState x:Name="VisualStateNarrow">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="{StaticResource NarrowMinWidth}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<!-- TODO: change properties for narrow view -->
<!--<Setter Target="stateTextBox.Text" Value="Narrow Visual State" />-->
</VisualState.Setters>
</VisualState>
<VisualState x:Name="VisualStateNormal">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="{StaticResource NormalMinWidth}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<!-- TODO: change properties for normal view -->
<!--<Setter Target="stateTextBox.Text" Value="Normal Visual State" />-->
</VisualState.Setters>
</VisualState>
<VisualState x:Name="VisualStateWide">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="{StaticResource WideMinWidth}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<!-- TODO: change properties for wide view -->
<!--<Setter Target="stateTextBox.Text" Value="Wide Visual State" />-->
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<controls:PageHeader x:Name="pageHeader" RelativePanel.AlignLeftWithPanel="True"
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignTopWithPanel="True" Text="Main Page">
<!-- secondary commands -->
<controls:PageHeader.SecondaryCommands>
<AppBarButton Click="{x:Bind ViewModel.GotoSettings}" Label="Settings" />
<AppBarButton Click="{x:Bind ViewModel.GotoPrivacy}" Label="Privacy" />
<AppBarButton Click="{x:Bind ViewModel.GotoAbout}" Label="About" />
</controls:PageHeader.SecondaryCommands>
</controls:PageHeader>
<validate:ControlWrapper PropertyName="FirstName"
RelativePanel.AlignLeftWithPanel="True"
RelativePanel.Below="pageHeader">
<TextBox Width="300"
Margin="12,0,0,0"
Header="First Name"
Text="{Binding FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</validate:ControlWrapper>
</RelativePanel>
As fas as I know I have the model with the ValidatableModelBase logic Applied, I do have the Validate:wrapper applied to the view, I've set the validator at the VM ctor or I could set it at model ctor.
My problem is where do i make the Calling logic???
Because I havent been able to trigger the UpdateSourceTrigger of my FirstName field.
What am i doing wrong??
To be more honest I dont know where to place the Validate method, because at the sample at github the validate method is call everytime you open the modal dialog window, but that is when you are navigating to that window and its field everytime they changed they are being validated, but in my case nothing happens, why?? Hopefully someone can help me out, since I'm new at template 10 and also at UWP.
Damn, I was so damn tired but my mistake was so god damn simple I forgot to set the DataContext at the Parent Panel, In this case I should had add DataContext ={Binding Model}
this at XAML within the relative panel or any other Panel.
I felt so noob to have made this question.
Related
I have a problem with displaying validation message in a Blazor app.
I have the following model (for testing purposes):
public class MyModel
{
[Required(ErrorMessage = "Amount is required")]
public int Amount { get; set; }
[Required(ErrorMessage = "NullableAmount is required")]
public int? NullableAmount { get; set; }
[RequiredIf(nameof(Foo), new[] { "bar" }, ErrorMessage = "Id is required")]
public int Id { get; set; }
[RequiredIf(nameof(Foo), new[] { "bar" }, ErrorMessage = "NullableId is required")]
public int? NullableId { get; set; }
public string Foo { get; set; }
}
I decorated the properties with the built-in RequiredAttribute, and created a custom RequiredIfAttribute, here's the code for that:
public class RequiredIfAttribute : ValidationAttribute
{
private readonly string _otherPropertyName;
private readonly string[] _otherPropertyValues;
public RequiredIfAttribute(string otherPropertyName, string[] otherPropertyValues)
{
_otherPropertyName = otherPropertyName;
_otherPropertyValues = otherPropertyValues;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var instance = validationContext.ObjectInstance;
var type = instance.GetType();
var propertyValue = type.GetProperty(_otherPropertyName)?.GetValue(instance);
if (!_otherPropertyValues.Contains(propertyValue?.ToString()))
{
return ValidationResult.Success;
}
if(value == null)
{
return new ValidationResult(ErrorMessage);
}
if(int.TryParse(value.ToString(), out var intValue))
{
if(intValue == 0)
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
}
I takes the name of the other property, the values for that property, and if the value of that other property matches one of the specified values, it checks if the property decorated with RequiredIf is null, or if it is an integer, 0. (I use it for a model with a nullable int property, which should not be null or 0 if the other property has a certain value).
On the WebAPI side it works fine, but in the Blazor form I have some problems. Here's my form:
<EditForm Model="MyModel" OnValidSubmit="OnSubmit">
<div style="display: flex; flex-direction: column; width: 300px;">
<DataAnnotationsValidator />
<ValidationSummary />
<label>
Foo:
<InputText #bind-Value="MyModel.Foo" />
</label>
<ValidationMessage For="#(() => MyModel.Foo)" />
<label>
Amount:
<InputNumber #bind-Value="MyModel.Amount" />
</label>
<ValidationMessage For="#(() => MyModel.Amount)" />
<label>
Null Amount:
<InputNumber #bind-Value="MyModel.NullableAmount" />
</label>
<ValidationMessage For="#(() => MyModel.NullableAmount)" />
<label>
Id:
<InputNumber #bind-Value="MyModel.Id" />
</label>
<ValidationMessage For="#(() => MyModel.Id)" />
<label>
Null Id:
<InputNumber #bind-Value="MyModel.NullableId" />
</label>
<ValidationMessage For="#(() => MyModel.NullableId)" />
<button type="submit">submit</button>
</div>
</EditForm>
I open the form, enter the value "bar" for Foo, than click submit, I get the following result:
Amount is valid, as it has the default value 0, and RequiredAttribute only checks for nulls.
NullAmount is invalid, it is shown in the ValidationSummary, the input field is also red, as it is invalid, and the validation message is also shown.
Id is invalid, as in the RequiredIf attribute I check int value against 0, it has the default 0 value, the error message is shown in the ValidationSummary, and here comes the interesting part, the input field is not red, and the validation message is not shown.
The same applies for the Nullable Id field, the error message is shown in the validation summary, but the field is not red, and the validation message is not shown.
Now, if I enter a valid value for Id and NullableId (the fields with my custom attribute), and after that I change Id to 0, and NullableId to empty, this is what happens:
Both input fields change to red, validation message is shown for both properties, but in the validation summary, both error messages are displayed twice.
Now, if I click submit, both input fields change to green, and the validation messages are gone, but still displayed in the summary.
I'm totally lost, don't know if I have a problem with my custom validation attribute, a problem in the Blazor component, or it is an unknown issue. The built-in Required attribute works fine, I just added it to the code to see if my form works with that.
Do you have any idea?
I've finally found the problem. In the validation attribute, when returning an error result, I have to pass memberName in the constructor.
return new ValidationResult(ErrorMessage, validationContext.MemberName)
Thank you so much #petyusa, I also ran into this issue and it was driving me nuts. I stumbled across your solution and it solved it immediately. However, I had to pass an array of string to the second argument (perhaps because I'm using .NET 5.0) instead of a simple string:
return new ValidationResult(ErrorMessage, new []{validationContext.MemberName});
Here is the situation that I have:
<Stack IsVisible="{Binding IsGood}" >
<Label Text="Is Good or Okay" />
</Stack>
<Stack IsVisible="{Binding IsOkay}" >
<Label Text="Is Good or Okay" />
</Stack>
What I would like to do is to somehow code it like this:
<Stack IsVisible="{Binding IsGood, IsOkay, Converter={StaticResource ORConverter} }" >
<Label Text="Is Good or Okay" />
</Stack>
You can't make a converter do that. A converter only takes one value. Instead, why not just use a property in the ViewModel which reflects this instead?
public bool AorB => A || B;
private bool _a;
public bool A
{
get => _a;
set
{
_a = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(AorB));
}
}
private bool _b;
public bool B
{
get => _b;
set
{
_b = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(AorB));
}
}
Now, if you are using MvvmCross, you could use the Value Combiners it provides. However, it will only work for MvvmCross's binding descriptions, which would look like:
If(Or(A, B), 'true', 'false')
That would need you to depend on MvvmCross though and switch to its bindings for these kind of operations.
I am trying to push the text from my Shell.SearchHandler to a command.
I have built this with SearchBar , but i need this to work in the Shell.SearchHandler
<Shell.SearchHandler >
<SearchHandler x:Name="searchBar"
Placeholder="Search..."
Command="{Binding PerformSearch}"
CommandParameter="{Binding Text, Source={x:Reference searchBar}}" TextColor="Black" BackgroundColor="LightGray" />
</Shell.SearchHandler>
And in my View model
public ICommand PerformSearch => new Command<string>((string query) =>
{
//do work
});
The command does execute but the query param is always null.
How can I get the text that was entered into the searchandler to be posted to the binding command?
CommandParameter="{Binding Text, Source={x:Reference searchBar}}"
After having a look at SearchHandler reference document as follow :
Query, of type string, the user entered text in the search box.
The reason is that SearchHandler not contains Text property .You should replace it with Query , then it can work .
<Shell.SearchHandler >
<SearchHandler x:Name="searchBar"
Placeholder="Search..."
Command="{Binding PerformSearch}"
CommandParameter="{Binding Query, Source={x:Reference searchBar}}"
TextColor="Black"
BackgroundColor="LightGray" />
</Shell.SearchHandler>
Note : Also can Custom a SearchHandler to monitor Query changed if needed .
Creare a MySearchHandler :
public class MySearchHandler : SearchHandler
{
protected override void OnQueryChanged(string oldValue, string newValue)
{
base.OnQueryChanged(oldValue, newValue);
if (string.IsNullOrWhiteSpace(newValue))
{
ItemsSource = null;
}
else
{
Console.WriteLine("search string " + newValue);
// here you can add code to deal with the viewmodel
//ItemsSource = MonkeyData.Monkeys
.Where(monkey => monkey.Name.ToLower().Contains(newValue.ToLower()))
.ToList<Animal>();
}
}
}
In Xaml :
<ContentPage ...
xmlns:controls="clr-namespace:Xaminals.Controls">
<Shell.SearchHandler>
<controls:MonkeySearchHandler Placeholder="Enter search term"
ShowsResults="true"
DisplayMemberName="Name" />
</Shell.SearchHandler>
...
</ContentPage>
Here is the official sample for reference , this need some time to research it.
I've got a problem trying to set a custom label on teh fly with a ribbon using Excel-DNA.
If I don't included the annotation "getLabel='GetLabel'" then the plugin loads fine. ie the ribbon tab is shown with 2 buttons andthe button callbacks work fine.
If I do inclue the property "getLabel='GetLabel'" then the plugin doesn't even load, ie onLoad isn't called and the ribbon tab doesn't show up in excel.
Can anyone see what I'm doing wrong here. I don't see any errors when running in the debugger.
Here is my DNA file. I've tried to base it off one of the samples so it's easier to follow.
<DnaLibrary Name="Emsx Addin" RuntimeVersion="v2.0">
<ExternalLibrary Path="EmsxExcelTech1.dll" />
<Reference AssemblyPath="System.Windows.Forms.dll" />
<!-- Some images that can be used in the Ribbon ui -->
<Image Name="M" Path="M.png" Pack="true" />
<CustomUI>
<customUI xmlns='http://schemas.microsoft.com/office/2009/07/customui' loadImage='LoadImage' onLoad='OnLoad'>
<ribbon>
<tabs>
<tab id='CustomTab' label='K2 Emsx' insertAfterMso='View'>
<group id='SampleGroup' label='Global Sheet Status'>
<button id='LoginCmd' label='Logon' image='M' onAction='OnLogonPressed' getLabel='GetLabel' />
<button id='BetaCmd' label='Use Beta Route' image='M' size='normal' onAction='RunTagMacro' tag='OnUseBetaRoutes' />
</group >
</tab>
</tabs>
</ribbon>
</customUI>
</CustomUI>
</DnaLibrary>
Here is my Ribbon derived C# file.
[ComVisible(true)]
public class EmsxRibbon : ExcelRibbon
{
private IRibbonUI ribbon = null;
public void OnLogonPressed(IRibbonControl control)
{
EmsxIntegration.Instance.Login();
MessageBox.Show("Hello from control " + control.Id);
if (ribbon != null)
{
ribbon.InvalidateControl(control.Id);
}
}
string GetLabel(IRibbonControl control)
{
if (control.Tag == "Logon")
{
return "Logon";
}
else
{
return "Logoff";
}
}
public static void OnUseBetaRoutes()
{
MessageBox.Show("Hello from 'ShowHelloMessage'.");
}
public void OnLoad(IRibbonUI ribbon)
{
this.ribbon = ribbon;
}
}
When you use the getLabel event, you should not use label property, so change
<button id='LoginCmd' label='Logon' image='M' onAction='OnLogonPressed' getLabel='GetLabel' />
to
<button id='LoginCmd' image='M' onAction='OnLogonPressed' getLabel='GetLabel' />
Hope this helps.
I want to bind to a resource (DynamicResource) and access properties on that resource, but is there a way to do that?
(I want to visualize the default values from constructor in the xaml editor in visual studio. Those cannot be seen when referencing an object through DataContext nor through a property added on my Window class...)
Not working xaml: (works in composer but not at runtime...)
<Window ... >
<Window.Resources>
<local:MyClass x:Key="myResource" />
</Window.Resources>
<StackPanel>
<Button Content="{Binding Source={DynamicResource myResource} Path=Property1}" />
<Button Content="{Binding Source={DynamicResource myResource} Path=Property2}" />
</StackPanel>
</Window>
with the class (which probably need to implement INotifyPropertyChanged):
public class MyClass
{
public MyClass()
{
this.Property1 = "Ok";
this.Property2 = "Cancel";
}
public string Property1 { get; set; }
public string Property2 { get; set; }
}
That's because the DynamicResource markup extension can only be used on a dependency property, because it will need to update it if the resource changes. And Binding.Source is not a dependency property...
As a workaround, you could set the DataContext of the button with the DynamicResource :
<Button DataContext="{DynamicResource myResource}" Content="{Binding Path=Property1}" />
<Button DataContext="{DynamicResource myResource}" Content="{Binding Path=Property2}" />
Abusing the DataContext of an unrelated object seems to be the easiest workaround.
In case you still need the DataContext of your control (MVVM anyone?), you can also create an invisible helper FrameworkElement elsewhere:
<FrameworkElement Visibility="Collapsed" x:Name="ControlBrushGetter" DataContext="
{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />
and later refer to it by using the name in the binding:
<SolidColorBrush Opacity="0.8"
Color="{Binding ElementName=ControlBrushGetter, Path=DataContext.Color}" />
Your designer will quite likely complain about not being able to resolve "Color" in the context of "object", but it will work fine at runtime.