I have a View which is binded to a ViewModel (that inherits from Observable) with a TextField and a Switch.
When i change the values, all is binded perfectly and I can see my new values.
But if I go to the home screen and then, go back to the app, the values are erased.
The view is a basic form like a customer CRUD.
Here is the XML :
<Page xmlns="http://schemas.nativescript.org/tns.xsd"
loaded="loaded">
<!-- ... -->
<TextView text="{{ valueA }}" />
<!-- ... -->
<Slider value="{{ valueB }}" minValue="1950" maxValue="2016" />
</Page
Here is the code-behind :
let viewModel: ViewModel;
let page: Page;
export function loaded(args: EventData) {
page = <Page>args.object;
setTimeout(function() {
viewModel = new ViewModel();
page.bindingContext = viewModel;
}, 0);
}
Do you have any idea why i loose my form data ?
Most likely, when you come back to the app, the loaded event is fired where you are constructing the view model. You should try constructing in a different page event (navigatedTo) or persist the data using the "application-settings" module. Then read the values from app-settings when you are constructing the observable view model. Hope this helps.
https://docs.nativescript.org/ApiReference/application-settings/HOW-TO.html
Related
I have something similar to this :
<j:states>
<js:State name="login"/>
<js:State name="loggued"/>
</j:states>
...
<j:Group includeIn="loggued" width="100%" height="100%">
<j:TabBar localId="tabbar" width="100%" change="ev_tab_change(event)">
<j:beads>
<js:ConstantBinding sourcePropertyName="tabBarDataSample" destinationPropertyName="dataProvider"/>
<j:AssignTabContent selectedContentProperty="content">
<j:content>
<j:TabBarContent width="100%" y="80" >
<royale:TB_One/>
<royale:TB_Two/>
</j:TabBarContent>
</j:content>
</j:AssignTabContent>
</j:beads>
</j:TabBar>
</j:Group>
<royale:TB_One/> and <royale:TB_Two/> are <j:SectionContent>
I need to trigger an event when TB_One or TB_Two is showing or tabbar selectedIndex was programmatically changed.
I have tried change event on TabBar, but this is not triggered when changing with selectedIndex
Is there an onShow event or valueCommit ?
(for now tabbar.dispatchEvent(new Event("change")); do the trick when programmatically change selectedIndex)
Used sdk is 0.9.8
Regards
you can do the following:
<!-- The TabBar -->
<j:TabBar initComplete="tabbarInitialized(event)"/>
...
<!-- Buttons to change the content programatically -->
<j:Button click="selectContentByIndex(0)" text="0"/>
<j:Button click="selectContentByIndex(1)" text="1"/>
...
<!-- as3 code in script -->
<fx:Script>
<![CDATA[
import org.apache.royale.events.Event;
// when tabbar is initialized make tabbar listen for internal event "selectionChanged"
// this is the event to use for programmatic changes
public function tabbarInitialized(event:Event):void
{
event.target.addEventListener("selectionChanged", contentChanged);
}
// The button change the selection programmatically
public function selectContentByIndex(index:int):void
{
tabbar.selectedIndex = index;
}
// Here run the code you want. I simply trace the new tabbat selected index and the item
// but you can calculate the content and operate over tha visual component
public function contentChanged(event:Event):void
{
trace("index:", tabbar.selectedIndex, "content:", tabbar.selectedItem);
}
]]>
</fx:Script>
My application viewModel responds to a user clicking a button to see test results:
private void AddDetailRows(List<QuizHistory> quizHistoryList)
{
quizDetails.Children.Clear();
quizDetails.Children.Add(AddData(quizHistoryList));
quizDetails.Children.Add(new LineTemplate());
}
Where quizDetails is the name of an element in the view.
But this doesn't work for me as the view model doesn't know what the view looks like and does not have access to the names of elements.
In a MVVM application, how is this problem solved?
You are completely right, that is not something that ViewModel is responsible of.
So, whatever you want to do with UI is not responsibility of the ViewModel.
If this is really the only option, then you can think of creating boolean properties in your VM and binding them to your views and then changing that boolean from false to true or vice versa on button click command which is binded to your VM.
To simplify it:
MyView.xaml
<StackLayout>
<Button Command="{Binding ShowHideQuizHistoryCommand}" ... />
<StackLayout x:Name="QuizHistory"
IsVisible={Binding ShowQuizHistory }>
//
</StackLayout>
</StackLayout>
MyViewModel.cs
private bool _showQuizHistory ;
public bool ShowQuizHistory
{
get { return _showQuizHistory ; }
set
{
_showQuizHistory = value;
OnPropertyChanged();
}
}
public ICommand ShowHideQuizHistoryCommand => new Command(() =>
{
ShowQuizHistory = !ShowQuizHistory;
});
So, this is just an example based on what you provided in question.
You can also use visual states, converters, triggers and behaviors in order to achieve this, but in my opinion this is the easiest way.
I have a Xamarin.Forms app with a master-detail page and it works well.
But I've recently needed to add a parameter to the constructor of the master page (AttendPageMaster), but now I need to pass this constructor.
How do I add a parameter to the xaml?
<MasterDetailPage.Master>
<pages:AttendPageMaster x:Name="MasterPage" />
</MasterDetailPage.Master>
The code behind page with constructor:
public AttendPageMaster(AttendanceViewModel viewModel)
{
}
Let me know if you need any more info.
You do not have to pass ViewModel to Page via constructor, you can set the Page's BindingContext:
<MasterDetailPage.Master>
<pages:AttendPageMaster x:Name="MasterPage">
<pages:AttendPageMaster.BindingContext>
<myViewModels:AttendanceViewModel />
</pages:AttendPageMaster.BindingContext>
</pages:AttendPageMaster>
</MasterDetailPage.Master>
This solution will work if your ViewModel does not expect any parameters in constructor. Otherwise you may consider using ViewModelLocator and DI to inject the constructor parameters.
Please note that myViewModels should be defined in the header of your XAML page as xmlns:myViewModels.
P.S.: Previously you mentioned that you got an exception while trying to use code behind approach. You could easily solve it by setting the Title property of the AttendPageMaster. Example:
new AttendPageMaster(new AttendanceViewModel()){ Title = " " };
I managed to do this from the code behind by creating the menu page in the constructor of the masterdetail and assigning it to the "Master" property:
AttendMasterPageMaster MasterPage;
public AttendMasterPage(AttendanceViewModel viewModel)
{
MasterPage = new AttendMasterPageMaster(viewModel);
Detail = new NavigationPage((Page)Activator.CreateInstance(typeof(StartPage), viewModel));
Master = MasterPage;
I am trying to figure out how to use NativeScript's ListView and I can not figure out how. I have yet to find any good tutorials and really need help. I need to be able to show and update (add and subtract items from) a ListView in NativeScript. Thanks in advance!
You can mostly find all the things about the ListView here but I will make a demonstration of ListView practice that I'm using in my app. I'm using MVC structurce so we will have page.xml, page.js and page-viewmodel.js. In page.xml, we will want to have a listview with like this:
<Page>
<ListView items="{{ myItems }}" loadMoreItems="loadMore">
<ListView.itemTemplate>
<Label text="{{ message }}" textWrap="true" />
</ListView.itemTemplate>
</ListView>
</Page>
The <ListView.itemTemplate> is where you define a prototype for an item of data array. You can use a normal array but I suggest using built-in class called ObservableArray for programmatically changing of any item or any property later on.
In the model, which is page-viewmodel.ts, we have:
var Observable = require("data/observable").Observable;
var ObservableArray = require("data/observable-array").ObservableArray;
public class PageViewModel extends Observable {
private _myItems = new ObservableArray<MyItem>()
get myItems(): {
return this._myItems
}
public loadItems() {
var dataArray = ["Red", "Blue", "Green"];
for (var i in dataArray) {
var item = MyItem(dataArray[i])
this._myItems.push(item);
}
}
}
var pageViewModel = new PageViewModel();
public class MyItem extends Observable {
public message: String;
constructor(value) {
this.message = value;
}
}
Finally, in the controller which is page.ts:
import {pageViewModel} from "./page-viewmodel"
exports function pageLoaded(args) {
var page = args.object;
page.bindingContext = pageViewModel
}
exports function loadMore(args) {
pageViewModel.loadItems();
}
In conclusion, you define the ListView as well as its prototype in XML. After that, the viewmodel (which is an Observable object) is where you handle the data(add item, delete item, load items from backend, etc). Finally, you import that viewmodel to the controller(page.js) and bind to page.bindingContext so that the XML can receive the data
P/S: I'm writing this in TypeScript. In Javascript, the implementation is basically the same, there are just a bit of difference in the syntax. For example here is how the page.js would look like:
var pageViewModel = require("./page-viewmodel");
function pageLoaded(args) {
var page = args.object;
page.bindingContext = pageViewModel
}
exports.pageLoaded = pageLoaded;
Scenario: I start out in my app's main page. I navigate to sub-page A, change a value, hit the back button and the bound TextBlock in the main page doesn't change. If I navigate to sub-page B, a TextBlock using that same binding changes. Likewise, if I go to page A again I see the changed value. If I exit the app, the new value shows up on the main page. It's just when using the back button that a refresh doesn't get triggered.
I've got all my INotifyPropertyChanged stuff working. Like I said, the binding works in every scenario besides navigating back to the main page. How do I send a message or otherwise trigger a refresh of the bindings on that page? Thanks!
Edit:
Based on the accepted answer from willmel, here's what I did:
My MainPage.xaml file has this markup:
<TextBlock Text="{Binding Title, Mode=OneWay}" />
My MainViewModel.cs file has this:
public string Title
{
get { return ProfileModel.Instance.DescriptionProfile.Title; }
}
And I added this to the MainViewModel constructor:
Messenger.Default.Register<PropertyChangedMessage<string>>(this,
(action) => DispatcherHelper.CheckBeginInvokeOnUI(
() => RaisePropertyChanged("Title")));
In another view I have the following markup:
<TextBox Grid.Row="1" Width="250" Height="100" Text="{Binding TitleEdit, Mode=TwoWay}" />
In its view model I use this when getting/setting a string:
public string TitleEdit
{
get { return ProfileModel.Instance.DescriptionProfile.Title; }
set
{
if (ProfileModel.Instance.DescriptionProfile.Title == value) return;
string oldValue = ProfileModel.Instance.DescriptionProfile.Title;
ProfileModel.Instance.DescriptionProfile.Title = value;
RaisePropertyChanged("Title", oldValue, value, true);
}
}
In your view model you want to be modified if a child page changes a property. (note here, the property is of type bool, but could be anything)
Messenger.Default.Register<PropertyChangedMessage<bool>>(this,
(action) => DispatcherHelper.CheckBeginInvokeOnUI(
() =>
{
MessageBox.Show(action.newValue.ToString());
//do what you want here (i.e. RaisePropertyChanged on a value they share)
}));
When you use RaisePropertyChanged in the child class, use the broadcasting overload.
RaisePropertyChanged("Preference", oldValue, value, true);
Finally, note that to use DispatcherHelper, you need to Add the following to your App constructor (App.xaml.cs)
DispatcherHelper.Initialize();