This is a follow up from my previous question (UWP Problems Setting GridView Items source using x:Bind).
In that case I was using the random access file data source from Mirosoft's Data virtualization sample and the grid was never being populated. The problem there was that I was not raising the property changed event. The grid now works well.
My problem now is that instead of a grid view I am trying to use the data source in a flip view control. The data source does get initialized, the change property notification is raised but nothing is shown in the flip view.
Can a flip view display data from a virtual collection?
Here is my model's code:
public class PhotoFlipViewViewModel : Mvvm.ViewModelBase
{
private FileDataSource _PicturesCollection;
public FileDataSource PicturesCollection
{
get
{
return _PicturesCollection;
}
set
{
if (_PicturesCollection != value)
{
_PicturesCollection = value;
RaisePropertyChanged(() => PicturesCollection);
}
}
}
public PhotoFlipViewViewModel()
{
initdata();
}
async void initdata()
{
StorageLibrary pictures = await StorageLibrary.GetLibraryAsync(KnownLibraryId.Pictures);
string path = pictures.SaveFolder.Path;
var source = await FileDataSource.GetDataSoure(path);
if (source.Count > 0)
{
PicturesCollection = source;
}
}
public override async Task OnNavigatedToAsync(object parameter, NavigationMode mode, IDictionary<string, object> suspensionState)
{
System.Diagnostics.Debug.WriteLine("PhotoFlipViewViewModel::OnNavigatedToAsync - event fired.");
if ( SessionState.ContainsKey("SelectedPhotoObjectFromGrid"))
{
await Task.CompletedTask;
}
public override async Task OnNavigatedFromAsync(IDictionary<string, object> suspensionState, bool suspending)
{
if (suspending)
{
}
await Task.CompletedTask;
}
public override async Task OnNavigatingFromAsync(NavigatingEventArgs args)
{
args.Cancel = false;
await Task.CompletedTask;
}
}
My FlipView in XAML:
<FlipView x:Name="PhotoOuterFlipView"
BorderBrush="Black"
BorderThickness="1"
VerticalAlignment="Top"
HorizontalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.PicturesCollection, Mode=OneWay}"
ItemTemplate="{StaticResource PictureFlipViewTemplate}">
<FlipView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</FlipView.ItemsPanel>
</FlipView>
Thanks, any advice is appreciated.
Related
I am receiving the below error :
Relative routing to shell elements is currently not supported. I am running Xamarin forms 5.0.0.2515.
The project navigates to the Items Journal Template View Model where I select an item. The item's name is then passed to the Finished Goods View Model where I will do a search based on the value passed. However I am receiving the above error even though I have already done this on other pages without an error.
I have my routing registered:
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(LocatePage), typeof(LocatePage));
Routing.RegisterRoute(nameof(FinishedGoodsPage), typeof(FinishedGoodsPage));
Routing.RegisterRoute(nameof(ItemJournalTemplatePage), typeof(ItemJournalTemplatePage));
}
ItemJournalTemplateViewModel: I call the Finished Goods page and pass it the item's name...
async void SelectedItemJournalTemplate(ItemJournalTemplate item)
{
if (item == null)
return;
await Shell.Current.GoToAsync($"{nameof(FinishedGoodsPage)}?{nameof(FinishedGoodsViewModel.PassedJournalBatchName)}={item.Name}");
}
and the page receiving the call FinishedGoodsViewModel:
[QueryProperty(nameof(PassedJournalBatchName), nameof(PassedJournalBatchName))]
public class FinishedGoodsViewModel:BaseViewModel
{
private string passedJournalBatchName;
public string PassedJournalBatchName
{
get => passedJournalBatchName;
set
{
passedJournalBatchName = value;
OnPropertyChanged(nameof(PassedJournalBatchName));
}
} .....
}
What makes this odd is that I use this on the FinishedGoodsViewModel to call the LocateViewModel and it works fine:
public async void UpdateLocation(object value)
{
SelectedValue = (FinishedGood)value;
await Shell.Current.GoToAsync($"{nameof(LocatePage)}" +
$"?{nameof(LocateViewModel.ItemNo)}={SelectedValue.Item_No}" +
$"&{nameof(LocateViewModel.BatchName)}={SelectedValue.Journal_Batch_Name}" +
$"&{nameof(LocateViewModel.TemplateName)}={SelectedValue.Journal_Template_Name}" +
$"&{nameof(LocateViewModel.LineNo)}={SelectedValue.Line_No}");
}
LocateViewModel:
[QueryProperty(nameof(ItemNo), nameof(ItemNo))]
[QueryProperty(nameof(BatchName), nameof(BatchName))]
[QueryProperty(nameof(TemplateName), nameof(TemplateName))]
[QueryProperty(nameof(LineNo), nameof(LineNo))]
public class LocateViewModel : BaseViewModel
{
private string batchName;
public string BatchName
{ get => batchName;
set
{
batchName = value;
OnPropertyChanged(nameof(BatchName));
}
}
private string templateName;
public string TemplateName
{
get => templateName;
set
{
templateName = value;
OnPropertyChanged(nameof(TemplateName));
}
}
private string lineNo;
public string LineNo
{
get => lineNo;
set
{
lineNo = value;
OnPropertyChanged(nameof(LineNo));
}
}
private string itemNo;
public string ItemNo
{
get => itemNo;
set
{
itemNo = value;
OnPropertyChanged(nameof(ItemNo));
}
}...
}
Can someone please tell me where I am going wrong here?
It looks like the answer was in the AppShell.xaml file. The original Flyout pointed to the FinsishedGoodsPage but I changed it to ItemJournalTemplatePage, allowing the user to first make a selection first.
<FlyoutItem Title="Finished Goods" Icon="placeholder.png">
<ShellContent Route="FinishedGoodsPage" ContentTemplate="{DataTemplate local:ItemJournalTemplatePage}" />
</FlyoutItem>
I forgot I also needed to change the Route. It probably would not go to the FinishedGoodsPage because it thought it was already on that page.
<FlyoutItem Title="Finished Goods" Icon="placeholder.png">
<ShellContent Route="ItemJournalTemplatePage" ContentTemplate="{DataTemplate local:ItemJournalTemplatePage}" />
</FlyoutItem>
I've recently started using Blazor. Is there a way to trigger form model validation only on submit, instead of live on each change?
Just for clarification, let's say I have something like this:
<EditForm Model="this" OnValidSubmit="SubmitForm">
<DataAnnotationsValidator />
<ValidationSummary />
<Label For="Name">Name</Label>
<InputText id="Name" name="Name" class="form-control" #bind-Value="Name"/>
<button type="submit">Save</button>
</EditForm>
#code {
[StringLength(10, ErrorMessage="Name too long")]
public string Name { get; set; }
private async Task SubmitForm()
{
// ...
// send a POST request
}
}
By default, it seems like the validity of the field and the error messages displayed in the ValidationSummary get re-evaluated on every change of the text input (e.g. as soon as I delete the 11th character from the input, the "too long" message disappears).
I would prefer if the displayed messages would remain frozen until the Submit button is clicked.
I suppose it would be possible to implement it by removing the ValidationSummary component and implementing a custom solution (e.g. displaying a List of error messages that's refreshed only on submit), but I was wondering if there is some idiomatic solution that I'm not aware of.
When validation occurs is controlled by the Validator you're using.
There are two events that you can receive from EditContext:
OnValidationRequested is invoked either when EditContext.Validate is called or as part of the form submission process.
OnFieldChanged is invoked every time a field value is changed.
A validator uses these events to trigger it's validation process, and outputs the results to the EditContext's ValidationMessageStore.
DataAnnotationsValidator wires up for both events and triggers validation whenever either is invoked.
There are other validators out there, and writing your own is not too difficult. Other than those from the usual control suppliers, there's Blazored, or mine. Mine is documented here - https://shauncurtis.github.io/articles/Blazor-Form-Validation.html. it has a DoValidationOnFieldChange setting!
#enet's answer sparked an alternative answer. Build your own DataAnnotationsValidator.
Here's the EditContext Extensions code. It's a modified version of the original MS Code with some extra control arguments.
using Microsoft.AspNetCore.Components.Forms;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
namespace StackOverflowAnswers;
public static class EditContextCustomValidationExtensions
{
public static IDisposable EnableCustomValidation(this EditContext editContext, bool doFieldValidation, bool clearMessageStore)
=> new DataAnnotationsEventSubscriptions(editContext, doFieldValidation, clearMessageStore);
private static event Action? OnClearCache;
private static void ClearCache(Type[]? _)
=> OnClearCache?.Invoke();
private sealed class DataAnnotationsEventSubscriptions : IDisposable
{
private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache = new();
private readonly EditContext _editContext;
private readonly ValidationMessageStore _messages;
private bool _doFieldValidation;
private bool _clearMessageStore;
public DataAnnotationsEventSubscriptions(EditContext editContext, bool doFieldValidation, bool clearMessageStore)
{
_doFieldValidation = doFieldValidation;
_clearMessageStore = clearMessageStore;
_editContext = editContext ?? throw new ArgumentNullException(nameof(editContext));
_messages = new ValidationMessageStore(_editContext);
if (doFieldValidation)
_editContext.OnFieldChanged += OnFieldChanged;
_editContext.OnValidationRequested += OnValidationRequested;
if (MetadataUpdater.IsSupported)
{
OnClearCache += ClearCache;
}
}
private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs)
{
var fieldIdentifier = eventArgs.FieldIdentifier;
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
var validationContext = new ValidationContext(fieldIdentifier.Model)
{
MemberName = propertyInfo.Name
};
var results = new List<ValidationResult>();
Validator.TryValidateProperty(propertyValue, validationContext, results);
_messages.Clear(fieldIdentifier);
foreach (var result in CollectionsMarshal.AsSpan(results))
{
_messages.Add(fieldIdentifier, result.ErrorMessage!);
}
// We have to notify even if there were no messages before and are still no messages now,
// because the "state" that changed might be the completion of some async validation task
_editContext.NotifyValidationStateChanged();
}
}
private void OnValidationRequested(object? sender, ValidationRequestedEventArgs e)
{
var validationContext = new ValidationContext(_editContext.Model);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true);
// Transfer results to the ValidationMessageStore
_messages.Clear();
foreach (var validationResult in validationResults)
{
if (validationResult == null)
{
continue;
}
var hasMemberNames = false;
foreach (var memberName in validationResult.MemberNames)
{
hasMemberNames = true;
_messages.Add(_editContext.Field(memberName), validationResult.ErrorMessage!);
}
if (!hasMemberNames)
{
_messages.Add(new FieldIdentifier(_editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!);
}
}
_editContext.NotifyValidationStateChanged();
}
public void Dispose()
{
if (_clearMessageStore)
_messages.Clear();
if (_doFieldValidation)
_editContext.OnFieldChanged -= OnFieldChanged;
_editContext.OnValidationRequested -= OnValidationRequested;
_editContext.NotifyValidationStateChanged();
if (MetadataUpdater.IsSupported)
{
OnClearCache -= ClearCache;
}
}
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo? propertyInfo)
{
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
{
// DataAnnotations only validates public properties, so that's all we'll look for
// If we can't find it, cache 'null' so we don't have to try again next time
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
// No need to lock, because it doesn't matter if we write the same value twice
_propertyInfoCache[cacheKey] = propertyInfo;
}
return propertyInfo != null;
}
internal void ClearCache()
=> _propertyInfoCache.Clear();
}
}
And the CustomValidation component:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace StackOverflowAnswers;
public class CustomValidation : ComponentBase, IDisposable
{
private IDisposable? _subscriptions;
private EditContext? _originalEditContext;
[CascadingParameter] EditContext? CurrentEditContext { get; set; }
[Parameter] public bool DoEditValidation { get; set; } = false;
/// <inheritdoc />
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " +
$"inside an EditForm.");
}
_subscriptions = CurrentEditContext.EnableCustomValidation(DoEditValidation, true);
_originalEditContext = CurrentEditContext;
}
/// <inheritdoc />
protected override void OnParametersSet()
{
if (CurrentEditContext != _originalEditContext)
{
// While we could support this, there's no known use case presently. Since InputBase doesn't support it,
// it's more understandable to have the same restriction.
throw new InvalidOperationException($"{GetType()} does not support changing the " +
$"{nameof(EditContext)} dynamically.");
}
}
/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
}
void IDisposable.Dispose()
{
_subscriptions?.Dispose();
_subscriptions = null;
Dispose(disposing: true);
}
}
You can use it like this:
<EditForm EditContext=this.editContext OnValidSubmit=OnValidSubmit>
<CustomValidation DoEditValidation=false/>
#*<DataAnnotationsValidator/>*#
<div class="row">
<div class="col-2">
Date:
</div>
<div class="col-10">
<InputDate #bind-Value=this.Record.Date></InputDate>
</div>
</div>
.......
So basically I'm try to to find out if a page was pushed modally.
Here is the code I have for my extension method:
public static bool IsModal(this Page page)
{
return page.Navigation.ModalStack.Any(p => page == p);
}
The issue is; p never equals page due to the fact p changes to NavigationPage during runtime although intellisense reports it as a type of Page at compile time.
I've tried casting p to a Page but the type does not change at runtime and intellisense just moans that the cast is redundant.
I call this extension by using CurrentPage.IsModal in my View Model. CurrentPage is a type of Page at compile time but then changes to NavigationPage at runtime.
The confusing thing is that during debugging, p has properties such as CurrentPage and RootPage which show in the debugger, but these are not accessible by using p.CurrentPage as the compiler complains they don't exist !?! I was going to try an compare these but I can't access them but can view them in the debugger.
You need to check the type of page first, a page without navigationbar can also be pushed modally:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private async void Button_Clicked(object sender, EventArgs e)
{
Page1 p = new Page1();
await this.Navigation.PushModalAsync(p, true);
bool b = PageExtensions.IsModal(p);
Console.WriteLine(b);
}
}
public static class PageExtensions
{
public static bool IsModal(this Page page)
{
if (page.GetType() == typeof(NavigationPage))
{
return page.Navigation.ModalStack.Any(p => ((NavigationPage)p).CurrentPage.Equals(page));
}
else
{
return page.Navigation.ModalStack.Any(p => p.Equals(page));
}
}
}
So this code works:
public static class PageExtensions
{
public static bool IsModal(this Page page)
{
return page.Navigation.ModalStack.Any(p=> ((NavigationPage) p).CurrentPage.Equals(page));
}
}
I'm concerned that is not safe as it assumes p is a Type of NavigationPage.
Can you try this, there could be typos, I wrote this freehand
public static bool IsModal(this Page page)
{
if (page.Navigation.ModalStack.Count > 0)
{
foreach (var thisPage in page.Navigation.ModalStack)
{
if (thisPage.Equals(page))
return true;
}
return false;
}
else
return false;
}
This is what I made to check the last pushed modal. Hope it helps to someone.
public async Task NewModalPagePushAsync(Page pageToOpen)
{
var lastModalPage = Application.Current.MainPage.Navigation.ModalStack;
if (lastModalPage.Count >= 1)
{
if (lastModalPage.Last().GetType().Name == pageToOpen.GetType().Name)
return;
}
await Application.Current.MainPage.Navigation.PushModalAsync(pageToOpen);
}
I'm trying to use databinding with the map api extension of windows phone toolkit. I'm doing :
<maps:Map x:Name="Map" Center="47.6, -122.3" ZoomLevel="12">
<maptk:MapExtensions.Children>
<maptk:MapItemsControl ItemsSource="{Binding PositionList}">
<maptk:MapItemsControl.ItemTemplate>
<DataTemplate>
<maptk:Pushpin GeoCoordinate="{Binding}" />
</DataTemplate>
</maptk:MapItemsControl.ItemTemplate>
</maptk:MapItemsControl>
</maptk:MapExtensions.Children>
</maps:Map>
with my code behind :
public partial class MainPage : PhoneApplicationPage, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string name= null)
{
if (object.Equals(variable, valeur)) return false;
variable = valeur;
NotifyPropertyChanged(name);
return true;
}
private IEnumerable<GeoCoordinate> positionList;
public IEnumerable<GeoCoordinate> PositionList
{
get { return positionList; }
set { NotifyPropertyChanged(ref positionList, value); }
}
public MainPage()
{
InitializeComponent();
PositionList = new List<GeoCoordinate>
{
new GeoCoordinate(47.6050338745117, -122.334243774414),
new GeoCoordinate(47.6045697927475, -122.329885661602),
new GeoCoordinate(47.605712890625, -122.330268859863),
new GeoCoordinate(47.6015319824219, -122.335113525391),
new GeoCoordinate(47.6056594848633, -122.334243774414)
};
DataContext = this;
}
}
But I can't see any pushpin on the map :(
What am I doing wrong ?
Note that If I use this in the code-behind file, it's working
MapExtensions.GetChildren(Map).OfType<MapItemsControl>().First().ItemsSource = PositionList;
Thanks in advance for your help,
Best regards
MapItemsControl derives from DependencyObject, not FrameworkElement so the DataContext does not propagate. Long story long... you can't data bind MapItemsControl from XAML unless you have some way to set the Source property of the Binding.
If the FindAncestor mode of RelativeSource worked on the phone, it might be possible to work around this but it apparently does not. This leaves us with either creating the binding in code or (more realistically) setting the ItemsSource in code.
I have a requirement to load images from a folder in the project to a stackpanel. Under each image a name should also be shown. The image folder can change at any time and the number of images can also change.(with a maximum of 50 images) I want to know if I can use data binding to handle this. I thought of having image ID's, their sources and the name for each image in an XML so that I can change that XML file whenever the image folder changes, without changing the rest of the code. Is that feasible? If so how? Can someone please guide me? Thank you in advance.
One solution would be to use a Filepicker to let the user select the images inside the folder, and then bind the selected images to an Itemscontrol. That itemscontrol can then be put inside the Stackpanel. Here's a quick sample using that solution.
Here's the codebehind for picking the image files:
private List<EditableImage> availableImagesList = new List<EditableImage>();
private async void FilePicker_Clicked(object sender, RoutedEventArgs e)
{
FileOpenPicker openPicker = new FileOpenPicker();
openPicker.ViewMode = PickerViewMode.List;
openPicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary;
//TODO: add supported image file types
openPicker.FileTypeFilter.Add("jpg,png,gif");
// We prompt the user to pick one or more files
IReadOnlyList<StorageFile> files = await openPicker.PickMultipleFilesAsync();
if (files.Count > 0)
{
availableImages.DataContext = null;
String fp = ""; // The path of the picked image
int index = availableImagesList.Count;
foreach (StorageFile file in files)
{
// Copying the selected image to local app data folder
//TODO: check if the selected file is actually and image
if (file != null )
{
StorageFile fileCopy = await file.CopyAsync(ApplicationData.Current.LocalFolder, file.DisplayName + file.FileType, NameCollisionOption.ReplaceExisting);
fp = fileCopy.Path;
}
//Creating the image
CustomImage picToAdd = new CustomImage(index+1, file.DisplayName, fp);
//Adding the image as an UI element to the app bar
availableImagesList.Add(picToAdd);
}
availableImages.DataContext = availableImagesList;
}
}
The CustomImage model:
public class CustomImage
{
private static Uri _baseUri = new Uri("ms-appx:///");
private int _id;
public int Id
{
get { return _id; }
set
{
this.SetProperty(ref this._id, value);
}
}
private string _name;
public string Name
{
get { return _name; }
set
{
this.SetProperty(ref this._name, value);
}
}
private string _imgPath;
public string ImgPath
{
get { return _imgPath; }
set
{
this.SetProperty(ref this._imgPath, value);
}
}
private String _imagePath = null;
private ImageSource _image = null;
public ImageSource Image
{
get
{
if (this._image == null && this._imagePath != null)
{
this._image = new BitmapImage(new Uri(CustomImage._baseUri, this._imagePath));
}
return this._image;
}
set
{
this._imagePath = null;
this.SetProperty(ref this._image, value);
}
}
public void SetImage(String path)
{
this._image = null;
this._imagePath = path;
this.OnPropertyChanged("Image");
}
public CustomImage(int id, string name, string imagepath)
{
SetImage(imagepath);
_id = id;
_name = name;
}
}
Here's the XAML for the ItemsControl inside the Stackpanel:
<StackPanel x:Name="loadedImages" HorizontalAlignment="Left" Orientation="Horizontal">
<!--Displaying the selected images in stackpanel-->
<ItemsControl ItemsSource="{Binding}" ItemsPanel="{StaticResource LoadedItemsPanel}">
<ItemsControl.ItemTemplate>
<!--The template for each object that is displayed as an UI element-->
<DataTemplate>
<Grid Height="88" Margin="2,0" Width="88" >
<Image Source="{Binding Image}"/>
<TextBlock Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
In your page resources, you must also define:
<ItemsPanelTemplate x:Key="LoadedItemsPanel">
<WrapGrid Orientation="Horizontal"/>
</ItemsPanelTemplate>