This is the function that changes location:
string restaurantId = (string)e.Parameter;
Console.WriteLine(restaurantId);
await Shell.Current.GoToAsync($"location?restaurantId={restaurantId}");
Param is not undefined and it is consoled there. And the page changes, but the query param is empty string:
[QueryProperty("RestaurantId", "restaurantId")]
internal partial class LocationModel : ObservableObject
{
[ObservableProperty]
public string restaurantId;
public LocationModel()
{
Console.WriteLine(RestaurantId); // EMPTY
}
}
This is AppShell.cs where routes are registered:
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("login", typeof(LoginPage));
Routing.RegisterRoute("home", typeof(MainPage));
Routing.RegisterRoute("location", typeof(LocationPage));
}
}
What am I doing wrong?
One way to fix this is to use the [QueryProperty] on your LocationPage (the Page) and not on the LocationModel (the ViewModel), and then pass it along from the Page to the ViewModel.
LocationPage.xaml.cs
[QueryProperty(nameof(Id), "restaurantId")]
public partial class LocationPage : ContentPage
{
private LocationModel _viewModel;
public string Id
{
set => _viewModel.RestaurantId = value;
}
public LocationPage()
{
InitializeComponent();
BindingContext = _viewModel = new LocationModel();
}
}
LocationModel
public partial class LocationModel : ObservableObject
{
[ObservableProperty]
public string restaurantId;
}
This way you can retrieve the query parameter and pass it along to the ViewModel. You'll have to implement the logic for loading data in the ViewModel according to the passed RestaurantId yourself. You could also do this using a method, such as Load(string restaurantId) instead of using a property.
You can read more about how query parameters work in the official documentation.
Related
Let's say I have a parameter in my ViewModel:
public string ChosenQualityParameter
{
get => DefectModel.SelectedQualDefectParameters?.Name ?? "Не выбран параметр";
}
and I have a class DefectModel with parameter SelectedQualDefectParameters.Name in it. I want to change the UI binded to ChosenQualityParameter, when theName parameter changes too.
But I don't know how to do this properly. Any suggestions? Thanks in advance.
You might define your ViewModel class like this:
public class ViewModel
{
private DefectModel _defectModel;
public ViewModel(DefectModel defectModel)
{
_defectModel = defectModel;
}
public string ChosenQualityParameter
{
get => _defectModel.SelectedQualDefectParameters?.Name ?? "Не выбран параметр";
}
}
I personally do not like such dependencies in viewmodels, but it might get the job done here. It seems to work in a console application anyway:
using System;
public class Parameters
{
public string Name { get; set; }
}
public class DefectModel
{
public Parameters SelectedQualDefectParameters { get; set; }
}
public class ViewModel
{
private DefectModel _defectModel;
public ViewModel(DefectModel defectModel)
{
_defectModel = defectModel;
}
public string ChosenQualityParameter
{
get => _defectModel.SelectedQualDefectParameters?.Name ?? "Не выбран параметр";
}
}
class Program
{
static void Main()
{
var defectModel = new DefectModel
{
SelectedQualDefectParameters = new Parameters
{
Name = "test"
}
};
var viewModel = new ViewModel(defectModel);
Console.WriteLine(viewModel.ChosenQualityParameter);
defectModel.SelectedQualDefectParameters.Name = "changed";
Console.WriteLine(viewModel.ChosenQualityParameter);
Console.ReadKey();
}
}
Thanks to #Knoop and #BartHofland, I've solved my issue by using INotifyPropertyChanged in my DefectModel and SelectedQualDefectParameters classes.
For setting ChosenQualityParameter I used MessagingCenter to send new value.
I want to fetch first record from AboutUs table and display as a content label.
I have created 4 classes in MVVM pattern.
First is Model class AboutUs.cs
[Table("tblAboutUs")]
public class AboutUs
{
[PrimaryKey, AutoIncrement, NotNull]
public int IDP { get; set; }
public string Content { get; set; }
}
Second is Data Access class
SQLiteAboutUs.cs
public class SQLiteAboutUs
{
private static readonly AsyncLock Mutex = new AsyncLock();
private SQLiteAsyncConnection dbConn;
public int StatusCode { get; set; }
public SQLiteAboutUs(ISQLitePlatform sqlitePlatform, string dbPath)
{
if (dbConn == null)
{
var connectionFunc = new Func<SQLiteConnectionWithLock>(() =>
new SQLiteConnectionWithLock
(
sqlitePlatform,
new SQLiteConnectionString(dbPath, storeDateTimeAsTicks: false)
));
dbConn = new SQLiteAsyncConnection(connectionFunc);
dbConn.CreateTableAsync<Model.AboutUs>();
}
}
public SQLiteAboutUs()
{
}
public async Task Save(Model.AboutUs content)
{
using (await Mutex.LockAsync().ConfigureAwait(false))
{
StatusCode = 0;
await dbConn.InsertAsync(new Model.AboutUs { Content = content.Content });
StatusCode = 1;
}
//For Get first Row from Table
public async Task<Model.AboutUs> GetAllData()
{
return await dbConn.Table<Model.AboutUs>().Where(x => x.IDP == 1).FirstOrDefaultAsync();
}
}
Third class ViewModel Class
AboutUsViewModel.cs
public class AboutUsViewModel
{
readonly SQLiteAboutUs _db;
public string AboutUsContent { get; set; }
//public string AboutUs;
public AboutUsViewModel()
{
_db = new SQLiteAboutUs();
}
public async void FirstRecord()
{
Model.AboutUs obj = await _db.GetAllData();
this.AboutUsContent = obj.Content;
}
}
Forth one is Code behind file of my xaml pages.
AboutUs.xaml.cs
public partial class AboutUs : ContentPage
{
readonly AboutUsViewModel aboutUsViewModel;
public AboutUs()
{
InitializeComponent();
aboutUsViewModel = new AboutUsViewModel();
aboutUsViewModel.FirstRecord();
lblContent.Text = aboutUsViewModel.AboutUsContent;
}
}
I have debug code but problem is In AboutUsViewModel.cs class in FirstRecord Method object can not be set that's why AboutUsContent string property is also not set.
I can't figure out why my debugger directly jump from GetAllData() method in SQLiteAboutUs.cs to label.text in code behind file of view?
Welcome in the wonderfull world of asynchronicity. I encourage you to read carefully about how await is working: How and When to use `async` and `await`
It is not a blocking call. Thus you create the view AboutUs. It creates the ViewModel AboutUsViewModel. It calls
aboutUsViewModel.FirstRecord();
But does not wait for the call to be complete (dont't forget you marked your FirstRecord function as async...)
So it calls
Model.AboutUs obj = await _db.GetAllData();
And directly return to the caller because of the await operator.
That's why it directly jump to
lblContent.Text = aboutUsViewModel.AboutUsContent;
What you would like is Something like
await aboutUsViewModel.FirstRecord();
To wait the call to be complete before going to the next line. But of course you can't do that, because you are in a constructor and you can't have an async constructor. And calling a database (or actually anything that could likely failed) in a constructor is a bad practice anyway.
I would advise you to let only InitializeComponent() in the constructor, and then use Something like the OnDataLoaded() event of your view to perform your async call with a await.
Hope it helps.
I am trying to pass the selected item from the list to the detail view, but myitem is null in the DetailViewmodel even though it is not in the MyViewModel.
MyViewModel.cs
public virtual ICommand ItemSelected
{
get
{
return new MvxCommand<MyViewModel>(item =>{SelectedItem = item;});
}
}
public MyViewModel SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
// myItem is NOT null here!!!
ShowViewModel<MyDetailViewModel>(new { date = Date, myItem = _selectedItem });
RaisePropertyChanged(() => SelectedItem);
}
}
MyDetailViewModel.cs
public class MyDetailViewModel: MvxViewModel
{
private MyViewModel _myItem;
public void Init(DateTime date, MyViewModel myItem = null)
{
// myItem is NULL here!!!
_myItem = myItem;
}
}
You can use a parameter object, because you can only pass one parameter. I usually crate a nested class Parameter for this.
public class MyDetailViewModel: MvxViewModel
{
private MyViewModel _myItem;
public class Parameter
{
public DateTime Date {get; set; }
public string Name {get; set;}
}
public void Init(Parameter param)
{
Name = param.Name;
}
}
and show the viewmodel like:
ShowViewModel<MyDetailViewModel>(new MyDetailViewModel.Parameter { Date = Date, Name = _selectedItem.Name });
But be aware!
The paramters cannot be complex due certain platform issues. You might have to pass only the Id of your Item within the Parameter object and then load MyItem in your Init function. Or you pass only a string and use serialization: https://stackoverflow.com/a/19059938/1489968
myItem is null because if you pass typed parameter to Init it should be the only parameter you pass. According to MvvmCross ViewModel Creation documentation:
Init() can come in several flavors:.
individual simply-Typed parameters
a single Typed parameter object with simply-Typed properties
as InitFromBundle() with an IMvxBundle parameter - this last flavor is always supported via the IMvxViewModel interface.
I am learning the whole new Universal Apps creation together with Prism and Unity, but I got a few questions I am not sure about:
I have the following simple data object:
public class Customer : IEditableObject, IEquatable<Customer>
{
private Customer backup;
public string Name { get; set; }
public string Surname { get; set; }
public DateTime DateOfBirth { get; set; }
public void BeginEdit()
{
this.backup = this.MemberwiseClone() as Customer;
}
public void CancelEdit()
{
this.Name = this.backup.Name;
this.Surname = this.backup.Surname;
this.DateOfBirth = this.backup.DateOfBirth;
}
public void EndEdit()
{
this.backup = this.MemberwiseClone() as Customer;
}
public bool WasChangeMade()
{
if (this.Equals(backup))
return false;
else
return true;
}
public bool Equals(Customer other)
{
return this.Name == other.Name &&
this.Surname == other.Surname &&
this.DateOfBirth == other.DateOfBirth;
}
}
Under my Main Page I have a simple ListBox, where I show collection of these Customers. Everything good so far.
Afterwards, when under my ListBox user selects any one of these Customer, then he can click Edit Settings button and edit properties of this selected Customer. It is a simple command:
cmd_EditCustomer = new DelegateCommand(() =>
{
_navigationService.Navigate(App.Experiences.Detail.ToString(), SelectedCustomer);
});
Which simply navigates to a new page (detail page, where user can do the changes) and the argument I pass here is the Selected Customer.
My DetailPage View Model looks like following:
public class DetailPageViewModel : ViewModel, Interfaces.IDetailPageViewModel
{
public DelegateCommand cmd_SaveChanges { get; set; }
public Customer SelectedCustomer { get; set; }
private readonly INavigationService _navigationService;
private readonly IDialogService _dialogService;
public DetailPageViewModel(INavigationService navigationService,
IDialogService dialogService)
{
_navigationService = navigationService;
_dialogService = dialogService;
InitializeCommands();
}
public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState)
{
this.SelectedCustomer = navigationParameter as Customer;
this.SelectedCustomer?.BeginEdit();
}
private void InitializeCommands()
{
cmd_SaveChanges = new DelegateCommand(() =>
{
SelectedCustomer?.EndEdit();
_dialogService.Show("Changes Saved!");
_navigationService.Navigate(App.Experiences.Main.ToString(), null);
});
}
}
As you can see, this is a very simple application, which I only use for learning purposes. Here are my questions:
1) Is it good to pass Selected Customer in such a way as I did? (in the parameter of the INavigationService), or should I implement other logic?
2) When user makes a change to the Selected Customer and clicks Save Changes (the only command you can see there), it does not update the original Customer (from my original collection). How is this possible? How to achieve, that my Customer will be updated? Should I create PubSubEvent for this?
EDIT:
I have managed to locate the error - when user navigates back to MainPage, my MainPageViewModel is re-initializes, which re-populates collection of items. The question now is - how can I keep MainWindowViewModel alive thorough the applications life?
Re-populates collection of items from what?
You just need to save a new values, for example if you populate your customers from DB you have to call DB and save changes before navigate back etc, so after that when MainPageViewModel would be re-initializes you'll get your changes and changes performed by another users.
In the end, I found out that this was not a good way how to hold data in your application.
Based on what I have read, I should have implemented Repository Strategy, which is only referenced in a ViewModel such as:
public MainPageViewModel(IDataRepository dataRepository, INavigationService navService, ...){etc.}
Example of a simplified interface:
public interface IDataRepository
{
List<string> GetListOfStrings();
string GetUserEnteredData();
void SetUserEnteredData(string data);
}
This is how you initialize it in UnityContainer:
_container.RegisterType<IDataRepository, DataRepository>();
You can read more from Patterns & Practices team in here:
https://prismwindowsruntime.codeplex.com/
let's say I've got this code (in Winforms):
public class SomeClass
{
public string Name { get; set; }
}
public partial class SomeControl : UserControl
{
private SomeClass inClass;
public string MyName { get; set; }
public SomeControl(SomeClass someClass)
{
InitializeComponent();
this.inClass = someClass;
SetupBinding();
}
private void SetupBinding()
{
this.DataBindings.Clear();
this.DataBindings.Add("MyName", this.inClass, "Name");
}
}
If I'll change the the value of SomeClass.Name outside the user control, the property MyName never changes. What am I doing wrong?
Thank you
SomeClass must impelement INotifyPropertyChanged or have event named NameChanged if you want bidirectional data binding. You can implement it yourself, but I highly recommend Fody.PropertyChanged project.
PS: I wrote some extension method to create binding more easier and refactoring friendly way. In your case it would look like this:
this.BindTo(inClass, c => c.MyName , m => m.Name);
The method itself:
public static class BindingExtensions
{
public static Binding BindTo<TControl, TControlProperty, TModel, TModelProperty>(this TControl control, TModel model, Expression<Func<TControl, TControlProperty>> controlProperty, Expression<Func<TModel, TModelProperty>> modelProperty, string format = null)
where TControl : Control
{
var controlPropertyName = ((MemberExpression)controlProperty.Body).Member.Name;
var modelPropertyName = ((MemberExpression)modelProperty.Body).Member.Name;
var formattingEnabled = !string.IsNullOrWhiteSpace(format);
var binding = control.DataBindings.Add(controlPropertyName, model, modelPropertyName, formattingEnabled, DataSourceUpdateMode.OnPropertyChanged);
if (formattingEnabled)
{
binding.FormatString = format;
}
return binding;
}
}