I'm doing a simple xamarin forms application where I have a TimeAttendance model, a list view page and a time entry page. When I select a time entry from the list it takes me to the add/edit page where I'm able to change all the dropdown/entry/timepicker values and it gets saved correctly.
The problem I'm having is that I'm calculating total hours based on start and end time. This value is getting saved correctly when I close the day but it's not updating in the UI even though the binding property is set. I can see in the backend code that the value gets updated but it's not reflected in the UI unless I close the day and open it again.
It looks like the binding from source to target it's only getting triggered on the first load, but I've been reading and these views are set to two-way binding by defualt.
xaml code:
<Label Text="Total Hours"/>
<Label x:Name="totalHours"
Text="{Binding TotalHours}"/>
<Button Text="CLOSE" Clicked="CloseDay_Clicked"/>
<Button Text="DELETE" Clicked="Delete_Clicked"/>
Code behind:
async void CloseDay_Clicked(object sender, EventArgs e)
{
var timeEntry = (TimeAttendance)BindingContext;
timeEntry.idUser = 1;
await App.Database.SaveTimeAttendanceAsync(timeEntry);
//await Navigation.PopAsync();
}
async void TimePicker_timeChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == TimePicker.TimeProperty.PropertyName)
{
var timeEntry = (TimeAttendance)BindingContext;
var totalHours = timeEntry.EndTime - timeEntry.StartTime;
timeEntry.TotalHours = totalHours.Hours + (totalHours.Minutes / 15) * 0.25;
}
}
The bindingContext is set form the list page:
async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs e)
{
await Navigation.PushAsync(new TimeAttendanceEntryPage
{
BindingContext = e.SelectedItem as TimeAttendance,
});
}
My timeattendance model:
public class TimeAttendance
{
[PrimaryKey, AutoIncrement]
public int ID { get; set; }
public int idUser { get; set; }
public DateTime Date { get; set; } = DateTime.Today;
public string Commodity { get; set; }
public string Exception { get; set; }
public string RequestType { get; set; }
public string RequestId { get; set; }
public TimeSpan StartTime { get; set; }
public TimeSpan EndTime { get; set; }
public string Activity { get; set; }
public double TotalHours { get; set; }
}
As I said, the code is working fine for updating values closing and saving, I just don't know why when I update TotalHours it gets saved correctly but I cannot see the change in the label that is binded directly to this value.
you just should let change your Timeattendance model,let it implement the INotifyPropertyChanged interface like this:
public class TimeAttendance : INotifyPropertyChanged
{
...
double totalHours;
public double TotalHours {
set
{
if (totalHours != value)
{
totalHours = value;
OnPropertyChanged("TotalHours");
}
}
get
{
return totalHours;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Related
i want to be able to take a picture with the emulator, have it create and save an item on the database and display properly on my main Item Page, how do I go about doing this?
i tried my best to only show what is important and related to this and therefore have cut a bunch of stuff out(for example my View for adding items isnt present because i THINK the only part relating to this is the button with a command binding to it leading to the viewmodel)
the reason im asking here is because there are no errors showing, the application doesnt start at all like this and im really confused, i appreciate any and all help and thank you in advance.
if you need any additional info about anything related to this please do ask and i will share it as soon as i see your messeage.
my Item.cs
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string LongDescription { get; set; }
public MediaFile Image { get; set; }
public string Quantity { get; set; }
public string TypeOfQuantity { get; set; }
}
my main page Listview (without the unimportant parts in XAML)
<ListView>
<ListView.ItemTemplate>
<DataTemplate x:DataType="model:Item">
<ViewCell Height="220" xct1:ShadowEffect.Radius="5">
<Grid Padding="7">
<Frame CornerRadius="15">
<StackLayout Orientation="Horizontal">
...// bunch of labels and what not//
</StackLayout>
</StackLayout>
</Frame>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
the Item Creating ViewModel (unrelated things cut out)
public string Name { get; set; }
public string ShDesc { get; set; }
public string LgDesc { get; set; }
public string Qty { get; set; }
public MediaFile Image { get; set; }
public string TypeOfQty { get; set; }
public AsyncCommand AddCommand { get; }
public AsyncCommand PicCommand { get; }
public ItemAddViewModel()
{
AddCommand = new AsyncCommand(Add);
PicCommand = new AsyncCommand(Pic);
}
async Task Add()
{
var name = Name;
var description = ShDesc;
var longdescription = LgDesc;
var qty = Qty;
var typeofqty = TypeOfQty;
var image = Image;
await ItemService.AddItem(name, description, longdescription, qty, typeofqty, image);
Name = ShDesc = LgDesc = Qty = TypeOfQty = null;
}
async Task Pic()
{
var photo = await CrossMedia.Current.TakePhotoAsync(
new StoreCameraMediaOptions
{
SaveToAlbum = true
});
Image = photo;
}
}
}
my Services and DB
public static class ItemService
{
static SQLiteAsyncConnection db;
static async Task Init()
{
//creats Database if it doesnt exist
if (db != null)
return;
var databasePath = Path.Combine(FileSystem.AppDataDirectory, "Mydata.db");
db = new SQLiteAsyncConnection(databasePath);
await db.CreateTableAsync<Item>();
}
//adds item to database
public static async Task AddItem(string Name, string Description,string LongDescription, string Quantity, string TypeOfQuantity, MediaFile image)
{
await Init();
var item = new Item
{
Name = Name,
LongDescription = LongDescription,
Description = Description,
Quantity = Quantity,
TypeOfQuantity = TypeOfQuantity,
Image = image
};
await db.InsertAsync(item);
}
I found this great post by Chris Sainty: Creating Bespoke Input Components for Blazor from Scratch. It is exactly what I need, but not with string, but with uploaded files IBrowserFile. So I have adapted and extended the example for me. The customized component displays the new files and saves it in my model, but in the CSS the status unfortunately stays on class="modified invalid".
I must be missing a small detail here. What is it? Thanks in advance for any hints.
Here is my code reduced to the essentials.
Selection.razor
#page "/selection"
#inherits ParentComponent<SelectionTestModel>
<PageComponent #ref="Page" Model="Model" StatusCode="StatusCode" PageType="PageType.Touch">
<PageBody>
<EditForm Model="Model" OnValidSubmit="Save">
<DataAnnotationsValidator />
<DocumentComponent #ref="DocumentUpload" #bind-Documents="Model.Files" />
</EditForm>
</PageBody>
</PageComponent>
#code {
private DocumentComponent DocumentUpload;
}
SelectionTestModel.cs
public class SelectionTestModel
{
public int? KeyID { get; set; }
/* ... */
[System.ComponentModel.DisplayName("Document")]
[System.ComponentModel.DataAnnotations.Display(Name = "Document")]
[System.ComponentModel.DataAnnotations.Range(2, 2, ErrorMessage = "You have to bring exactly two files!")]
public List<DocumentModel> Files { get; set; } = new List<DocumentModel>();
}
DocumentModel
public class DocumentModel
{
public int? Id { get; set; }
public string Reference { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public string ContentType { get; set; }
public string Content { get; set; } /*file as base64 string*/
}
DocumentComponent.razor
#using System.Linq.Expressions
<div class="dropzone rounded #_dropClass #_validClass">
<InputFile id="inputDrop" multiple
ondragover="event.preventDefault()"
ondragstart="event.dataTransfer.setData('', event.target.id)"
accept="#AllowedFileTypes"
OnChange="OnInputFileChange"
#ondragenter="HandleDragEnter"
#ondragleave="HandleDragLeave" />
#*...*#
</div>
#code {
[CascadingParameter] public EditContext EditContext { get; set; }
[Parameter] public List<DocumentModel> Documents { get; set; } = new List<DocumentModel>();
[Parameter] public EventCallback<List<DocumentModel>> DocumentsChanged { get; set; }
[Parameter] public Expression<Func<List<DocumentModel>>> DocumentsExpression { get; set; }
/*...*/
public List<string> AllowedFileTypes { get; set; } = new List<string> { ".pdf", /*...*/ };
private FieldIdentifier _fieldIdentifier;
private string _validClass => EditContext?.FieldCssClass(_fieldIdentifier) ?? null;
protected override void OnInitialized()
{
base.OnInitialized();
_fieldIdentifier = FieldIdentifier.Create(DocumentsExpression);
}
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
// validation: do we accept the file (content type, amount of files, size)
if (e.FileCount == 1) // keep it simple for this example
{
// read from IBrowserFile and return DocumentModel in memory only
Documents.Add(await SaveFile(e.File));
await DocumentsChanged.InvokeAsync(Documents);
EditContext?.NotifyFieldChanged(_fieldIdentifier);
}
}
/*...*/
}
How does it behave in the browser (Chrome)
After loading the page everything looks as expected.
After that I upload a single file. So I have one file and I expect two. The validation turns red and I get "modified invalid". So far everything is great.
Finally I drag another file into the component and get two files. I can also see this in the model. But unfortunately the class attribute "modified valid" is not set.
Thanks again for any advice
I dug too deep in the wrong direction and didn't see the obvious.
The problem is that there is an attribute set in the model that does not throw an error, but also cannot validate.
The Range attribute is not for lists and therefore the model could never validate. With an own attribute I could work around this.
SelectionTestModel.cs
[Library.Validation.Attribute.ListRange(2, 2)]
public List<DocumentModel> Files { get; set; } = new List<DocumentModel>();
ListRangeAttribute.cs
namespace Library.Validation.Attribute
{
public class ListRangeAttribute : ValidationAttribute
{
public int Minimum { get; set; }
public int Maximum { get; set; }
public ListRangeAttribute(int minimum = 0, int maximum = int.MaxValue)
{
Minimum = minimum > 0 ? minimum : 0;
Maximum = maximum;
}
public string GetErrorMessage(string displayName) { /* ... */ }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var list = value as IList;
if (list == null)
{
throw new InvalidOperationException($"Attribute {nameof(ListRangeAttribute)} must be on a property of type {nameof(IList)}.");
}
if ((list?.Count ?? 0) < Minimum || (list?.Count ?? int.MaxValue) > Maximum)
{
return new ValidationResult(GetErrorMessage(validationContext.DisplayName), new[] { validationContext.MemberName });
}
return ValidationResult.Success;
}
}
}
I hope this post can help others.
Remaining: Now I am left with a new mystery.
Why does the validation text disappear after a save button click, which could not be saved due to an invalid state of the model!?
I have a problem with my Xamarin.Forms app, specifically with a Picker which does not show the actual value of the SelectedItem property. I searched all available threads but none solved my issue. I have a ListView page with an ObservableCollection of "Surgery" objects, each having a property of type "Category" (see below).
public class Surgery
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
public DateTime Date { get; set; }
[ForeignKey(typeof(Category))]
public int CategoryId { get; set; }
[ManyToOne(CascadeOperations = CascadeOperation.All)]
public Category Category { get; set; }
}
public class Category
{
[PrimaryKey, AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<Surgery> Surgeries { get; set; }
}
I also have an EditPage with a picker:
<Picker Title="Category" x:Name="categoryPicker"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
ItemDisplayBinding="{Binding Name}"
/>
And the corresponding EditPageViewModel:
public class EditPageViewModel : BaseViewModel
{
public Surgery Surgery { get; set; }
public Command LoadCategoriesCommand
{
get
{
return new Command(async () => await LoadCategoriesExecute());
}
}
public List<Category> categories;
public EditPageViewModel(Surgery surgery = null)
{
LoadCategoriesCommand.Execute(null);
Surgery = surgery ?? new Surgery { Date = DateTime.Today };
}
async Task LoadCategoriesExecute()
{
if (IsBusy)
return;
IsBusy = true;
try
{
Categories = await App.DataService.GetAllCategoriesAsync();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
finally
{
IsBusy = false;
}
}
public string SurgeryName
{
get { return Surgery.Name; }
set
{
Surgery.Name = value;
OnPropertyChanged();
}
}
public DateTime SurgeryDate
{
get { return Surgery.Date; }
set
{
Surgery.Date = value;
OnPropertyChanged();
}
}
public Category SelectedCategory
{
get { return Surgery.Category; }
set
{
if(Surgery.Category != value)
Surgery.Category = value;
OnPropertyChanged();
}
}
public List<Category> Categories
{
get { return categories; }
set
{
categories = value;
OnPropertyChanged();
}
}
}
Creating a new Surgery object works perfectly fine with this code. It is saved to the database und displayed in the ListView page. The picker displays the List correctly and the Category can be chosen (which is also reflected in the UI).
THE PROBLEM: When I try to edit a Surgery object with a set Category property, the EditPage displays all properties and the corresponding value correctly except for the picker. The items are loaded and are also bound to the ItemSource - however, the picker is not set to the actual Category property of the Surgery object. Any ideas?
You need to define properties in your models with INotifyPropertyChanged like in your viewmodel.
I have a ListView that contains images, the images are fetched from a server. The first API call I make gets the data in var people. I load the listview with a placeholder image, then run the 2nd API call to get the images for each item in the listview. I receive a byte[] as the image, and I convert it to an ImageSource. I have a search button at the top of the page that I set binding to TempImage which uses the byte[] as its source, and it changes to the images that are loaded. So the conversion of byte[] to ImageSource is fine. The initial setting of p.PictureImageSource = "name_circle.png" also works correctly. However, setting p.PictureImageSource to the converted byte[] does NOT work. It never changes from the initial "name_circle.png". Any ideas?
var people = peopleModel.Response;
if(people.Count == 0)
{
ShowNoResults = true;
}
else
{
ShowNoResults = false;
Results = peopleModel.Response;
foreach (PersonViewModel p in Results)
{
p.Initials = p.FirstName[0].ToString() + p.LastName[0];
p.PictureImageSource = "name_circle.png";
}
}
//must do 2 seperate loops so the initials load before going on with 2nd search
foreach (PersonViewModel p in Results)
{
IsBusy = false;
var peopleImage = await peopleService.GetPersonImage("p.Email");
if ((peopleImage.Error == null) && (peopleImage.Response != null))
{
p.Picture = peopleImage.Response;
byte[] imageAsBytes = (byte[])peopleImage.Response;
p.PictureImageSource = ImageSource.FromStream(() => new MemoryStream(imageAsBytes));
TempImage = ImageSource.FromStream(() => new MemoryStream(imageAsBytes));
}
}
OnPropertyChanged();
-
public class PersonViewModel : INotifyPropertyChanged
{
public WorkstationViewModel WorkstationDetail { get; set; }
public List<PointViewModel> Points { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName { get; set; }
public string Initials { get; set; }
public string Email { get; set; }
public string ID { get; set; }
public string Department { get; set; }
public string BuildingName { get; set; }
public string SiteID { get; set; }
public string BuildingID { get; set; }
public string FloorNumber { get; set; }
public string FloorID { get; set; }
public string Workstation { get; set; }
public string Title { get; set; }
public string Phone { get; set; }
public byte[] Picture { get; set; }
public ImageSource PictureImageSource { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
}
Apineda was correct in thinking the listview wasn't refreshing at all with the new data. I separated the two API calls into two different methods (not sure if this part is entirely necessary). After the completion of the 2nd call, I set the listview's ItemSource to null, then back to the Results value, forcing a refreshing. Now the images show.
searchGestureRecognizer.Tapped += async (s, e) => {
await _viewModel.GetResults();
await _viewModel.GetImagesForResults();
resultsListView.ItemsSource = null;
resultsListView.ItemsSource = _viewModel.Results;
};
I have a class in WCF:
[DataContract]
public class Usuario
{
[DataMember]
public int ID { get; set; }
[DataMember]
public string Nombre { get; set; }
[DataMember]
public string Contraseña { get; set; }
}
In WP7 Proyect read ObservableCollection from WCF and load a ListPicker with:
lpUsuarios.ItemSource = listUsuarios;
This work ok.
Now, in WP7 use "Usuario _usuario = new Usuario()" for local variable.
The problem is, if I save the variable _usuario with IsolatedStorage and then load and apply in: lpUsuarios.SelectedItem = _usuario, causes the error: SelectedItem must always be set to a valid value.
Example:
Usuarios _usuario = new Usuario();
private void ButtonSave_Click(object sender, RoutedEventArgs e)
{
var settings = IsolatedStorageSettings.ApplicationSettings;
_usuario = lpUsuarios.SelectedItem as Usuario;
settings.Add("test", _usuario);
settings.Save();
}
Now, close the application and then open:
private void ButtonLoad_Click(object sender, RoutedEventArgs e)
{
settings.TryGetValue<Usuario>("test", out _usuario);
lpUsuarios.SelectedItem = _usuario; <--- ERROR SelectedItem must....
}
In vs2010 debug, when open the application and load the variable _usuario, value is correct, but not work.
Error in: SelectedItem must always be set to a valid value, in ListPicker.cs
Location in ListPicker.cs:
// Synchronize SelectedIndex property
if (!_updatingSelection)
{
_updatingSelection = true;
SelectedIndex = newValueIndex;
_updatingSelection = false;
}
Any solution?
If I use SelectedIndex, work ok, thanks Etch.
But now, the problem is if I want use:
public override bool Equals(object obj)
{
return ID == (obj as Users).ID;
}
public override int GetHashCode()
{
throw new NotImplementedException();
}
Where implement that, ¿in WCF class, in ModelView?
In XAML use:
SelectedItem={Binding SelectedUser, Mode=TwoWay}"
And in ModelView use:
private Usuario selectedUser;
public Usuario SelectedUser
{
get
{
return selectedUser;
} //----------------if i use modelview, the error is produced here
set
{
selectedUser= value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("SelectedUser"));
}
}
}
WCF class:
[DataContract]
public class Usuario
{
[DataMember]
public int ID { get; set; }
[DataMember]
public string Nombre { get; set; }
[DataMember]
public string Contraseña { get; set; }
}
Your collection doesn't have item that you want to select. Even if looks the same, smell the same, but it's a different object. Your Users class must override Equals method for this:
public class Users
{
public int ID { get; set; }
public string Nombre { get; set; }
public override bool Equals(object obj)
{
return ID == (obj as Users).ID;
}
public override int GetHashCode()
{
throw new NotImplementedException();
}
}
You can't select an item that is not one of the items in the collection bound to ItemsSource.
I ran into this problem just the other day. Exact problem. There is a bug in the Listpicker. There was a link I have at home that goes into the details of it, but the simple work around is to do what you did and use the SelectedIndex property instead.
By the way I found another question on this exact topic.