I am writing a program using Xamarin, Shell and MVVM.I want to send a parameter to next page and I am using the following code:
await Shell.Current.GoToAsync($"//{nameof(CopyBooksPage)}?RegisteredUserId={registeredUser.Id}");
Question: How to get the parameter before binding?
First wrong solution:
In CopyBooksPage.xaml I have binding to properties in VM but I have no affiliation with the VM.
I do this:
[XamlCompilation(XamlCompilationOptions.Compile)]
[QueryProperty(nameof(RegisteredUserId), "RegisteredUserId")]
public partial class CopyBooksPage : ContentPage
{
private int _registeredUserId;
public int RegisteredUserId
{
get { return _registeredUserId; }
set
{
_registeredUserId = value;
CopyBooksViewModel copyBooksViewModel = App.GetViewModel<CopyBooksViewModel>();
copyBooksViewModel.RegisteredUserId = _registeredUserId;
BindingContext = copyBooksViewModel;
copyBooksViewModel.RefreshBinding();
}
}
public CopyBooksPage()
{
InitializeComponent();
}
}
Call order:
construktor
properties RegisteredUserId
The problem is twofold.
After calling the constructor, I get information about binding errors (because there is no VM). It's not annoying but I want (need) to get rid of it.
I do the binding only in the propertis and this causes the problem that for each VM propertis I have to call the OnPropertyChanged method to refresh the binding. And this is troublesome for me. I do not want to do it.
Second wrong solution
Code behind:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CopyBooksPage : ContentPage
{
public CopyBooksPage()
{
InitializeComponent();
BindingContext = App.GetViewModel<CopyBooksViewModel>();
}
}
ViewModel:
class CopyBooksViewModel : BaseViewModel, IQueryAttributable
{
private int registeredUserId;
//read from database
private CopyBook copyBookModel;
public string BookTitle
{
get { return copyBookModel.Title; }
set
{
copyBookModel.Title = value;
OnPropertyChanged(nameof(Title));
}
}
public CopyBooksViewModel()
{
}
public void ApplyQueryAttributes(IDictionary<string, string> query)
{
if (query.ContainsKey("RegisteredUserId"))
{
registeredUserId = int.Parse(HttpUtility.UrlDecode(query["RegisteredUserId"]));
copyBookModel = ReadFromDatabase(registeredUserId);
}
}
}
Call order:
construktor
Binding
method ApplyQueryAttributes
When binding the BookTitle, I reference the copyBookModel which is null. I could secure it.
The problem is that the ApplyQueryAttributes method is called last. In it again I would have to call OnPropertyChanged for all propertis. I do not want to do it.
Related
I am struggling with Text binding in my WPF app.
Lets imagine that I have another working app (ex. windows service) with some data in it.
In my WPF app I would like to have folder "DATA" with class where data are introduced and in same folder another class which would include a void which will query my windows service
I would like to show this data in my WPF window.
To make it simpler - one class with data, one class with data changing and WPF window with showing this data.
Unfortunately I can not achieve this... When I am executing below code, my window is showing 0 instead 123.
I would like to achive that my window will show value 123.
file "Database.cs" in folder "Data" in project "example"
namespace example.Data
{
public class Database
{
private int _testInt = 0;
public int testInt
{
get { return _testInt; }
set { _testInt = value; }
}
}
}
file "Query.cs" in folder "Data" in project "example"
namespace example.Data
{
public class Query
{
public Database _database;
public void execute()
{
_database = new Database();
_database.testInt = 123;
}
}
}
file "MainWindow.xaml.cs" in project "example"
namespace example
{
public partial class MainWindow : Window
{
public Data.Database _database;
public Data.Query _query;
public int testInt
{
get { return _database.testInt; }
set { _database.testInt = value; OnPropertyChanged(); }
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
_database = new Data.Database();
_query = new Data.Query();
_query.execute();
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
#endregion
}
}
File MainWindow.xaml
<Window>
<TextBlock Text="{Binding testInt}"
Foreground="White"
FontSize="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="20,10,10,0" />
</Window>
P.S. If I will put
_database.testInt = 987;
to MainWindow.xaml.cs it is working properly - window is showing value 987 in textblock.
You have multiple instances of the Database object, a new one each time Query.execute is called and one in MainWindow constructor.
It's the data in the later that is displayed.
You should modify the content of this instance to see any change, for that, you must inject it in the Query object:
_query = new Data.Query(_database);
// ...
public class Query
{
private readonly Database _database;
public Query(Database database)
{
_database = database;
}
public void Execute()
{
_database.testInt = 123;
}
}
Finally you need a way to notify the view that the content as changed, that why Database should implement INotifyPropertyChanged.
But at this point it's badly named, because it's a model in the MVVM pattern.
you need to implement INotifyPropertyChanged
public partial class MainWindow : Window, INotifyPropertyChanged
from the MVVM view, I think these answers from Orace and Jason are on a good way, both do not solve the problem completely.
Let the Mainwindow implement INotifyPropertyChanged
Let the query accept the new value:
public void execute(int value)
{
//_database = new Database();
// inject _database like in the answer above
_database.testInt = value;
}
When your testInt changes, let the _query deliver the change down to the "database" (btw: you do it vice versa) See code below:
`public int testInt
{get { return _database.testInt; }
`set { _query.execute(value); OnPropertyChanged(); }`
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
_database = new Data.Database();
// the property change will change both the view and the model
testInt = 987;
}
Well, you have changed both model and view with one property change then, Good or not?!
Just for future users. There is small bug in Orace's answer: (It should be without "readonly" parameter, because below You are writing to it.
private Database _database;
public Query(Database database)
{
_database = database;
}
I have a viewmodel, in which I regularly update one of my parameters from a bluetooth module. (I have a breakpoint in my setter, so I'm sure its being updated)
The parameters does correctly update in my viewmodel, and I'm sure that my binding is correct in my view.
I suspect that my propertyChanged? method is the one that bugs my code:
public class Viewmodel : INotifyPropertyChanged
{
public Viewmodel()
{
}
public string CurrentValue
{
get
{
return _currentValue;
}
set
{
if (_currentValue!= value)
{
_currentValue= value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(CurrentValue));
}
}
}
}
View:
<Label Text="{Binding BindingContext.CurrentValue, Source={x:Reference Name=MyCarousel}}"/>
View CodeBehind:
public partial class View: ContentPage
{
public TemperaturePage()
{
InitializeComponent();
MyCarousel.BindingContext = new ViewModel();
}
}
Fyi, I'm using Xabre BLE to facilitate the connection, and my characteristic listener which updates my viewmodel looks as follows:
public async void GetValuesFromCharacteristic()
{
Viewmodel viewModel = new Viewmodel();
Characteristic.ValueUpdated += (s, a) =>
{
Console.WriteLine(Characteristic.Value[7].ToString());
Xamarin.Forms.Device.BeginInvokeOnMainThread(() =>
{
viewModel.CurrentValue = Characteristic.Value[7].ToString();
});
};
await Characteristic.StartUpdatesAsync();
}
To me it seems, that PropertyChanged? remains null, thus why nothing is updated.
As mentioned by dymanoid, the parameter to PropertyChangedEventArgs should be a string with the name of the property that changed.
The ViewModel also needs to implement INotifyPropertyChanged. It does not in your example code, but I'm assuming that's just a sample definition for this question.
I'm developing a Windows application (UWP) that has two pages, I want the best practice to pass parameters between pages.
it's my scenario:
We have two pages, each open and remain at the middle of the screen and a Button on each page, which send the message to the other page when we click on it.
I also want to pass information continuously and repeatedly.
in Page1.cs:
Page2 page2;
public Page1()
{
this.InitializeComponent();
CreatPage2();
}
// creat page 2
private async void CreatPage2()
{
var NewWindow = CoreApplication.CreateNewView();
int NewWindowid = 0;
await NewWindow.Dispatcher.RunAsync(CoreDispatcherPriority.High, () =>
{
Frame newframe = new Frame();
newframe.Navigate(typeof(Page2), this);
Window.Current.Content = newframe;
Window.Current.Activate();
ApplicationView.GetForCurrentView().Title = "page2";
NewWindowid = ApplicationView.GetForCurrentView().Id;
});
await Windows.UI.ViewManagement.ApplicationViewSwitcher.TryShowAsStandaloneAsync(NewWindowid);
}
//Button
private void ChangeP2_Click(object sender, RoutedEventArgs e)
{
// send a message to the texblock in the page2
page2.TexBlock2.Text=$"From page1 :{e.ToString()}";
// change text color of the texblock in the page2
page2.Foreground= new SolidColorBrush(Windows.UI.Colors.Red);
}
in Page2.cs:
Page1 page1;
protected override void OnNavigatedTo(NavigationEventArgs e)
{
page1 = e.Parameter as Page1;
base.OnNavigatedTo(e);
}
public Page2()
{
this.InitializeComponent();
}
//Button
private void ChangeP1_Click(object sender, RoutedEventArgs e)
{
// send a message to the texblock in the page1
page1.TexBlock1.Text=$"From page2 :{e.ToString()}";
// change text color of the texblock in the page1
page1.Foreground= new SolidColorBrush(Windows.UI.Colors.Red);
}
the above code just work for the page2 to the page1. (it can change the textblock of pagea).
Please help me, I can't find a solution that work on two pages
Naah… the best way is to use a standard pattern that consist of an app ViewModel class, which contains all the common app data that you want to use in the logic layer.
I always do it like this:
1) I use the MainPage automatically created as the "shell" of the app, with a property that is the AppViewModel.
The MainPage (and thus the AppViewModel) can be accessed from everywhere in the app, by setting itself as a static field in its own class.
This is the code, simpler than you think:
public sealed partial class MainPage : Page
{
public AppViewModel ViewModel { get; set; } = new AppViewModel();
public static MainPage Current { get; set; }
public MainPage()
{
this.InitializeComponent();
Current = this;
}
}
2) The AppViewModel itself is a class that must implement the INotifyPropertyChanged interface, in order to enable bindable properties and functions.
It is common, among developers, to create a base class that implements it and then derive all the classes that needs bindable properties from it.
Here it is:
public class BaseBind : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T storage, T value,
[CallerMemberName] String propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
}
Then you derive AppViewModel class (and all the other model and viewmodel classes) from it… populating it with all the common properties that you need to share across pages.
I have even added a derived property, in order to show how you can share even multiple data types at once, and a function:
public class AppViewModel : BaseBind
{
public AppViewModel()
{
// ...
}
// All common app data
private string sampleCommonString;
public String SampleCommonString
{
get { return sampleCommonString; }
set { SetProperty(ref sampleCommonString, value); OnPropertyChanged(nameof(SampleDerivedProperty1)); OnPropertyChanged(nameof(SampleDerivedProperty2)); }
}
public String SampleDerivedProperty1 => "return something based on SampleCommonString";
public String SampleDerivedProperty2
{
get
{
<<evaluate SampleCommonString>>
return "Same thing as SampleDerivedProperty1, but more explicit";
}
}
// This is a property that you can use for functions and internal logic… but it CAN'T be binded
public String SampleNOTBindableProperty { get; set; }
public void SampleFunction()
{
// Insert code here.
// The function has to be with NO parameters, in order to work with simple {x:Bind} markup.
// If your function has to access some specific data, you can create a new bindable (or non) property, just as the ones above, and memorize the data there.
}
}
3) Then, in order to access all this from another Page, just create an AppViewModel field in that page, as seen below:
public sealed partial class SecondPage : Page
{
public AppViewModel ViewModel => MainPage.Current.ViewModel;
public SecondPage()
{
this.InitializeComponent();
}
}
...and you can easily bind XAML controls properties to the AppViewModel itself:
<TextBlock Text="{x:Bind ViewModel.SampleCommonString, Mode=OneWay}"/>
<Button Content="Sample content" Click="{x:Bind ViewModel.SampleFunction}"/>
(Mode=OneWay is for real-time binding, in order that the property is immediately updated even in the UI, while Mode=TwoWay is used for those properties that can be edited from the control itself, by the user, in order to interact with app logic).
Hope this helped.
Best regards and happy new year.
I am trying to pass a value to a view model from another view model before navigating to the page attached to that view model.
I was previously passing it to the view, then passing it to the view model. This seems like a clumsy way of doing things.
I am not using any kind of framework so that is not an option.
At the moment the property is set as static and this works but im not sure if this is good practice.
The code:
View model 1:
This command opens the new page:
public void OpenRouteDetails()
{
RouteStopPopOverViewModel.RouteName = "TestRoute";
App.Page.Navigation.PushAsync(new RouteStopPopOverView());
}
View model 2: (RouteStopPopOverViewModel)
public static string RouteName { get; set; }
This does work but I would prefer not to use static as a way to achieve this.
Is there some way to set the RouteName property without using static or passing it through view-> view model.
I have seen some answers about this but they don't seem to answer to question clearly.
Share a controller class between view models.
The same instance has to be supplied to the constructor in both view models.
So you can set values, and listen for events in both view models.
The controller class becomes the intermediary.
public class SharedController : IControlSomething
{
private string _sharedValue;
public string SharedValue
{
get => _sharedValue;
set
{
if (_sharedValue == value)
return;
_sharedValue = value;
OnSharedValueUpdated();
}
}
public event EventHandler SharedValueUpdated;
protected virtual void OnSharedValueUpdated()
{
SharedValueUpdated?.Invoke(this, EventArgs.Empty);
}
}
public class ViewModel1
{
private readonly IControlSomething _controller;
public ViewModel1(IControlSomething controller)
{
// Save to access controller values in commands
_controller = controller;
_controller.SharedValueUpdated += (sender, args) =>
{
// Handle value update event
};
}
}
public class ViewModel2
{
private readonly IControlSomething _controller;
public ViewModel2(IControlSomething controller)
{
// Save to access controller values in commands
_controller = controller;
_controller.SharedValueUpdated += (sender, args) =>
{
// Handle value update event
};
}
}
here the sample you can achieve your requirement easily with navigation
public class ViewModelFrom : BaseViewModel
{
async Task ExecuteCommand()
{
string routeName="value to trasfer";
Navigation.PushAsync(new View(routeName));
}
}
public partial class View : ContentPage
{
public View(string routeName)
{
InitializeComponent();
BindingContext = new ViewModelTo(routeName);
}
}
public class ViewModelTo : BaseViewModel
{
public string RouteName { get; set; }
public ViewModelTo(string routeName)
{
RouteName=routeName;
}
}
If there is a hierarchy you could express that in a parent to both of them.
public class Route
{
private string Name;
}
public class RouteSelectedArgs : EventArgs
{
public Route Selected { get; set; }
}
public interface IRouteSelection
{
event EventHandler<RouteSelectedArgs> RouteSelected;
}
public interface IRouteDetails { }
public class RouteWizard
{
public UserControl view { get; set; }
private IRouteSelection _selection;
private IRouteDetails _details;
public RouteWizard(IRouteSelection selection, IRouteDetails details)
{
_selection = selection;
_details = details;
_selection.RouteSelected += Selection_RouteSelected;
view = MakeView(_selection);
}
private void Selection_RouteSelected(object sender, RouteSelectedArgs e)
{
_selection.RouteSelected -= Selection_RouteSelected;
view = MakeView(_details, e.Selected);
}
private UserControl MakeView(params object[] args)
{
////magic
throw new NotImplementedException();
}
}
As you are using the MVVM pattern, you can use one of the many MVVM Frameworks to achieve this.
I use FreshMvvm and it allow me to pass parameters between view models like this
await CoreMethods.PushPageModel<SecondPageModel>(myParameter, false);
Then in SecondPageModel I can see access the parameters in the Init method
private MyParamType _myParameter;
public override void Init(object initData)
{
base.Init(initData);
var param = initData as MyParamType;
if (param != null)
{
_myParameter = param;
}
}
You can find more details about FreshMvvm here although most MVVM frameworks have similar functionality.
I have the following simple navigation flow:
ViewModel1=>ViewModel2=>ViewModel3
When ViewModel3 is closed, I publish using the Message plugin to ViewModel some information which needs to get added to list in ViewModel1. Unfortunately nothing happens (I raise NotifChanged). In my opinion it happens because it's not called from UI.
What is the best way to achieve a refreshing list? I don't see any method in the ViewModel which is called when ViewModel is back from another ViewModel i.e. when ViewModel3 is closed.
EDIT:
Example Code:
public class WarehouseInViewModel : MvxViewModel
{
public WarehouseInViewModel(IMvxMessenger messenger)
{
mvxMessenger = messenger;
myToken = mvxMessenger.Subscribe<mAcceptMessage>(OnMyMessageArrived);
}
public override void Start()
{
base.Start();
}
private readonly IMvxMessenger mvxMessenger;
private MvxSubscriptionToken myToken;
private List<mProduct> productItems;
public List<mProduct> ProductItems
{
get { return productItems; }
set
{
productItems = value;
RaisePropertyChanged(() => ProductItems);
}
}
private MvxCommand<AcceptMenuItem> buttonCommand;
public ICommand ButtonCommand
{
get
{
return buttonCommand = buttonCommand ?? new MvxCommand<AcceptMenuItem>(MenuClick);
}
}
private void OnMyMessageArrived(mAcceptMessage myMessage)
{
mProduct product = mProduct.GetById(myMessage.ProductId);
//Something more ...
// There I want to update my Listview which is binded to ProductItems
ProductItems.Add(product);
RaisePropertyChanged(() => ProductItems);
}
public async void MenuClick(AcceptMenuItem menu)
{
ShowViewModel<WarehouseInScanViewModel>();
}
}
And the most important method from Third ViewModel (we assume that secont ViewMOdel only opens third, and it's closed properly)):
public void ButtonNextClick()
{
vxMessenger.Publish(new mAcceptMessage(this, productId, scannedLocation.Id, productQuantity));
Close(this);
}
So, when I'm back from third VM, I want to refreash ListView. I can't do it from OnMyMessageArrived because it's not in UI thread.
You could add a GlobalVars.cs static class (probably create Services folder and put it inside so it will be cleaner) with static fields and use that as the data source of your list in ViewModel1. Then, all you need to do is add the data on the List in GlobalVars.cs from ViewModel3.
Services/GlobalVars.cs
public static class GlobalVars
{
public static List<CustomClass> aGlobalVariable;
}
ViewModel1.cs
using Services;
...
public CustomClass LocalVariable
{
get { return GlobalVars.aGlobalVariable; }
set
{
GlobalVars.aGlobalVariable = value;
...
}
}
//refresh purpose
public void refresh()
{
RaisePropertyChanged(() => LocalVariable);
}
ViewModel3.cs
//just add the GlobalVars List when you need it
GlobalVars.aGlobalVariable.Add(new CustomClass());
To refresh your ListView, when you get back to the ViewModel1, you just simply call refresh() method from the activity. Below is how you access ViewModel in activity.
View1.cs
ViewModel1 vm;
...
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
...
vm = ViewModel as ViewModel1;
...
}
//what you want to do is refreshing the list on resume
protected override void OnResume()
{
base.OnResume()
vm.refresh();
... //other things you might want to do
}
Hope this answer could help.
To make it work you should either change your List ProductItems to ObservableColletion or do this
ProductItems.Add(product);
ProductItems = ProductItems.ToList();