Page1 creates and databinds to a new instance of Foo (call it theFoo)
theFoo.Name is set via textbox from Page1
theFoo is saved to a globally accessible data structure (list of Foos, whatever)
navigate from Page1 to Page2
Page2 databinds to the global list of Foos, to display all Foo instances
When I do this, I can verify that the Foo instance is added to the global list. But Page2 never shows any Foos.
If I manually add Foos to the global list (in code instead of from Page1), then navigate to Page2 without ever navigating to Page1 at all, I see Foos displayed in Page2.
What's the issue here?
Update:
Here's some relevant code...
Item.cs (Data and global storage structure)
public class Item
{
public string Name { get; set; }
}
internal static class ItemRepos
{
private static List<Item> _items = new List<Item>();
public static Item New()
{
return new Item();
}
public static int Count
{
get { return _items.Count; }
}
public static IEnumerable<Item> GetAll()
{
return _items;
}
public static Item Get( string name )
{
return _items.SingleOrDefault( item => item.Name == name );
}
public static void Save( Item item )
{
if ( _items.Contains( item ) == false )
{
_items.Add( item );
}
}
public static void Remove( Item item )
{
_items.Remove( item );
}
}
Relevant Page1.xaml fragment
<TextBlock Text="Name:"
Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="5" />
<TextBox x:Name="txtName"
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Name}" />
Page1.xaml.cs
public partial class ItemDetail : PhoneApplicationPage
{
public ItemDetail()
{
InitializeComponent();
}
protected override void OnNavigatedTo( NavigationEventArgs e )
{
base.OnNavigatedTo( e );
this.DataContext = ItemRepos.New();
}
private void Nav( object sender, EventArgs e )
{
NavigationService.Navigate( new Uri( "/Page2.xaml", UriKind.RelativeOrAbsolute ) );
}
private void Save( object sender, EventArgs e )
{
ItemRepos.Save( (Item) this.DataContext );
}
}
Relevant Page2.xaml fragment
<controls:PivotItem Header="A-Z">
<ListBox x:Name="listAZ"
ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</controls:PivotItem>
Page2.xaml.cs
public partial class ViewItems : PhoneApplicationPage
{
public ViewItems()
{
InitializeComponent();
}
protected override void OnNavigatedTo( NavigationEventArgs e )
{
base.OnNavigatedTo( e );
this.DataContext = ItemRepos.GetAll();
}
}
Without seeing any of your code, its hard to know exactly what is going on, but I'll try guessing. If you add some code to your question, I can be more helpful. Here is a list of possible problems.
The list of Foos isn't actually static (globally accessible to both pages) and they are referencing different lists.
You Page1 isn't successfully updating the list of Foos.
If the list of Foos is an ObservableCollection, somewhere you are instatiating it more than once.
If the list of Foos isn't an ObservableCollection, you aren't setting up the data binding correctly between the two pages.
Update:
Try using the following instead of _items in Item.cs.
public static ObservableCollection<Item> Items = new ObservableCollection<Item>();
Then, change the DataContext of page 2 to use the collection directly:
protected override void OnNavigatedTo( NavigationEventArgs e )
{
base.OnNavigatedTo( e );
this.DataContext = ItemRepos.Items;
}
Related
I have a collectionview that is bound to an ObservableRangeCollectionin my ViewModel.
In my ViewModel there is a Method that runs onAppearing and I want my ColletionViewto be filled from there, but when I do so the collectionveiw dose not display the content only when i reload the content is shown.
View:
<RefreshView Grid.Row="1"
Grid.RowSpan="2"
Command="{Binding RefreshCommand}"
IsRefreshing="{Binding IsBusy, Mode=OneWay}">
<RefreshView.RefreshColor>
<OnPlatform x:TypeArguments="Color">
<On Platform="iOS" Value="White"/>
</OnPlatform>
</RefreshView.RefreshColor>
<CollectionView x:Name="Collection"
ItemsSource="{Binding Locations, Mode=OneWay}"
ItemTemplate="{StaticResource ListDataTemplate}"
RemainingItemsThresholdReachedCommand="{Binding LoadMoreCommand}"
RemainingItemsThreshold="10"
SelectionMode="Single"
BackgroundColor="Transparent"
ItemsLayout="VerticalList"
SelectedItem="{Binding SelectedItem}"
SelectionChangedCommand="{Binding SelectedCommand}">
<CollectionView.EmptyView>
<StackLayout Padding="12">
<Label HorizontalOptions="Center" Text="Keine Daten vorhanden!" TextColor="White"/>
</StackLayout>
</CollectionView.EmptyView>
</CollectionView>
</RefreshView>
ViewModel:
namespace YourPartys.ViewModels
{
public class ListViewModel : ViewModelBase
{
#region Variables
#endregion
#region Propertys
LocationModel selectedItem;
public LocationModel SelectedItem
{
get => selectedItem;
set => SetProperty(ref selectedItem, value);
}
public ObservableRangeCollection<LocationModel> Locations { get;set; } = new ObservableRangeCollection<LocationModel>();
double distance;
public double Distance
{
get => distance;
set => SetProperty(ref distance, value);
}
#endregion
#region Commands
public ICommand FilterButtonCommand { get; }
public ICommand RefreshCommand { get; }
public ICommand SelectedCommand { get; }
public ICommand LoadMoreCommand { get; }
#endregion
//Constructor
public ListViewModel()
{
FilterButtonCommand = new Command(OpenFilter);
RefreshCommand = new AsyncCommand(Refresh);
SelectedCommand = new AsyncCommand(Select);
}
public override async void VModelActive(Page sender, EventArgs eventArgs)
{
base.VModelActive(sender, eventArgs);
var locs = await FirestoreService.GetLocations("Locations");
Locations.AddRange(locs);
}
private void OpenFilter(object obj)
{
PopupNavigation.Instance.PushAsync(new ListFilterPage());
}
private async Task Refresh()
{
IsBusy = true;
var locs = await FirestoreService.GetLocations("Locations");
Locations.AddRange(locs);
IsBusy = false;
}
private async Task Select()
{
if (SelectedItem == null)
return;
var route = $"{nameof(DetailPage)}?Locationid={SelectedItem.Locationid}";
SelectedItem = null;
await AppShell.Current.GoToAsync(route);
}
}
}
There are several problems in your demo.
1.Since you set the BindingContext for your page in xaml as follows:
<ContentPage.BindingContext>
<viewmodels:MainViewModel/>
</ContentPage.BindingContext>
you didn't need to recreate another object MainViewModel in a CS file and reference it. These are two different objects.
MainViewModel viewModel;
viewModel = new MainViewModel();
protected override void OnAppearing()
{
base.OnAppearing();
viewModel.VModelActive(this, EventArgs.Empty);
}
So, you can get the BindingContext in MainPage.xaml.cs in function OnAppearing as follows:
protected override void OnAppearing()
{
base.OnAppearing();
viewModel = (MainViewModel)this.BindingContext;
viewModel.VModelActive(this, EventArgs.Empty);
}
The whole code is
public partial class MainPage : ContentPage
{
MainViewModel viewModel;
public MainPage()
{
InitializeComponent();
// viewModel = new MainViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
viewModel = (MainViewModel)this.BindingContext;
viewModel.VModelActive(this, EventArgs.Empty);
}
}
2.when we set the text color of the Label to White,this makes it hard to see the text,so you can reset it to another color,for example Black:
<Label Text="{Binding Name}"
FontSize="30"
TextColor="White"/>
I do not load a small data from the API, in C# code, they are loaded in advance and everything seems to be fine, but as soon as I open the page where ItemsSource = "{Binding BigData}", my UI is blocked for 10 seconds.
Are there any ideas to open the page first, then start loading data without blocking the UI?
I would to suggest you can kick off a task in your view models constructor that loads the data. Using Async and await to load bid data.
I do one sample that using ListView to display 100000 records.
<StackLayout>
<Label Text="test ui in xamarin.forms asyn" />
<ActivityIndicator IsRunning="{Binding isBusy}" IsVisible="{Binding isBusy}" />
<ListView x:Name="listview1" ItemsSource="{Binding Items}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal">
<Label Text="{Binding name}" />
<Label HorizontalOptions="CenterAndExpand" Text="{Binding age}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
public partial class Page19 : ContentPage
{
public Page19()
{
InitializeComponent();
this.BindingContext = new ItemsViewModel();
}
}
public class ItemsViewModel:ViewModelBase
{
private bool _isBusy;
public bool isBusy
{
get { return _isBusy; }
set
{
_isBusy = value;
RaisePropertyChanged("isBusy");
}
}
public ObservableCollection<people> Items { get; set; }
public ItemsViewModel()
{
Items = new ObservableCollection<people>();
isBusy = true;
Task.Run(async () => await LoadItems());
}
public async Task LoadItems()
{
var items = new ObservableCollection<people>(); // new collection
if (isBusy)
{
await Task.Delay(10000);
// var loadedItems = ItemsService.LoadItemsDirectory();
//foreach (var item in loadedItems)
// items.Add(item);
for (int i = 0; i < 100000; i++)
{
people p = new people();
p.name = "people " + i;
p.age = i;
items.Add(p); // items are added to the new collection
}
Items = items; // swap the collection for the new one
RaisePropertyChanged(nameof(Items)); // raise a property change in whatever way is right for your VM
isBusy = false;
}
}
}
public class people
{
public string name { get; set; }
public int age { get; set; }
}
ViewModelBase is one class that implementing INotifyPropertyChanged
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
I need to pass data from a ListView to a TodoDetail page where I have a Telerik DataForm, but I don't know how to make it work. If I use normal Xamarin Forms controls it works fine, but need it to work with the Telerik DataForm control.
Here is my code:
Todo.xaml
list item tapped handler
private async void ToDoTaskTap(object sender, ItemTappedEventArgs e)
{
var user = ToDoTask.SelectedItem as tblEmpTask;
if (user != null)
{
var mainViewModel = BindingContext as MainViewModel;
if (mainViewModel != null)
{
mainViewModel.Selected = user;
await Navigation.PushAsync(new ToDoDetail(mainViewModel));
}
}
}
tblEmpTask.cs
public class tblEmpTask
{
public string strTaskName { get; set; }
}
TodoDetail.xaml
<telerikInput:RadDataForm x:Name="dataForm">
<telerikInput:RadDataForm.Source>
<local1:MainViewModel />
</telerikInput:RadDataForm.Source>
</telerikInput:RadDataForm>
MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged
{
public tblEmpTask Selected
{
get { return _Selected; }
set
{
_Selected = value;
OnPropertChanged();
}
}
[DisplayOptions(Header = "Name")]
public string Name
{
get { return this.Selected.strTaskName; }
set
{
if (value != this.Selected.strTaskName)
{
this.Selected.strTaskName = value;
OnPropertChanged();
}
}
}
}
You must add a binding between the SelectedItem and the ListView
Here's an example:
ViewModel:
public List<object> ItemsSource { get; set; }
public object SelectedItem {
set { SelectedItemChanged(value); }
}
async void SelectedItemChanged(object value) {
await App.Current.MainPage.Navigation.PushAsync(new AboutPage(SelectedItem));
}
Page:
<ListView
ItemsSource="{Binding ItemsSource}"
SelectedItem="{Binding SelectedItem, Mode=OneWayToSource}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding .}"></Label>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I think I got a threading problem in my UWP app.
I want to do a very simple thing:
a UI with 2 numeric fields;
if a numeric value is typed in field1, I want field2 to be set with a ratio of field1 (example: field2 = ratio * field1).
I am using x:Bind and TextChanging events. For unknown reasons, I wasn't able in the XAML to "call" the TextChanging event without having an exception at startup. Therefore, I am using the Loaded event.
Here's my model class, simply called MyModel:
public class MyModel : INotifyPropertyChanged
{
private readonly uint r1 = 3;
private uint _field1;
public uint Field1
{
get { return this._field1; }
set
{
this.Set(ref this._field1, value);
if (value == 0)
{
Field2 = 0;
}
else
{
Field2 = value * r1;
}
}
}
private uint _field2;
public uint Field2
{
get { return this._field2; }
set
{
this.Set(ref this._field2, value);
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisedPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
else
{
storage = value;
this.RaisedPropertyChanged(propertyName);
return true;
}
}
}
My ViewModel:
public class MyModelViewModel : INotifyPropertyChanged
{
public MyModel MyModel { get; set; }
public MyModelViewModel()
{
// Initialisation de notre page
this.MyModel = new MyModel()
{
Field1 = 0
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
my code behind (I'm filtering the input to avoid a cast exception):
public sealed partial class MainPage : Page
{
public MyModelViewModel ViewModel { get; set; } = new MyModelViewModel();
public MainPage()
{
this.InitializeComponent();
}
private void InitField1(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
field1.TextChanging += field1_TextChanging;
}
private void InitField2(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
field2.TextChanging += field2_TextChanging;
}
private void field1_TextChanging(TextBox sender, TextBoxTextChangingEventArgs args)
{
var error = errorTextBlock;
error.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
Regex regex = new Regex("[^0-9]+"); // All but numeric
if (regex.IsMatch(sender.Text))
{
error.Text = "Non numeric char";
error.Visibility = Windows.UI.Xaml.Visibility.Visible;
sender.Text = this.ViewModel.MyModel.Field1.ToString();
}
else
{
this.ViewModel.MyModel.Field1 = Convert.ToUInt32(sender.Text);
}
}
private void field2_TextChanging(TextBox sender, TextBoxTextChangingEventArgs args)
{
var error = errorTextBlock;
error.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
Regex regex = new Regex("[^0-9]+");
if (regex.IsMatch(sender.Text))
{
error.Text = "Non numeric char";
error.Visibility = Windows.UI.Xaml.Visibility.Visible;
sender.Text = this.ViewModel.MyModel.Field2.ToString();
}
else
{
this.ViewModel.MyModel.Field2 = Convert.ToUInt32(sender.Text);
}
}
}
Finally, my XAML:
<TextBlock Grid.Row="0" Grid.Column="0" x:Name="errorTextBlock" Text="" Visibility="Collapsed" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="Field 1" />
<TextBox Grid.Row="1" Grid.Column="1" x:Name="field1" Text="{x:Bind ViewModel.MyModel.Field1, Mode=OneWay}" Loaded="InitField1" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="Field 2" />
<TextBox Grid.Row="2" Grid.Column="1" x:Name="field2" Text="{x:Bind ViewModel.MyModel.Field2, Mode=OneWay}" Loaded="InitField2" />
At runtime, if I type a non numeric char in field1, the input is filtered, field1 returns to its previous value without the screen "blinking" (that's why I use the TextChanging event and not the TextChanged). Perfect! But if I type a numeric char, field1 is correctly updated (I can see that with breakpoint), but when field2 is set, I got a native exception when RaisedPropertyChanged is called:
I'm suspecting some kind of threading error, but I'm pretty new to this kind of development. Any idea? Thanks!
Updated to use a separate 'Model' class
Here's how you can create a text box that when a number (integer) is entered into it another text box shows the entered number multiplied by another number.
Here's the UI. Note the Mode used for each binding and the second textbox is readonly because that's just for display.
<StackPanel>
<TextBlock Text="Value 1" />
<TextBox Text="{x:Bind ViewModel.MyModel.Value1, Mode=TwoWay}" />
<TextBlock Text="Value 2" />
<TextBox Text="{x:Bind ViewModel.MyModel.Value2, Mode=OneWay}" IsReadOnly="True" />
</StackPanel>
On the page I declare my Model
public MyViewModel ViewModel { get; set; } = new MyViewModel();
My ViewModel is very simple
public class MyViewModel
{
public MyModel MyModel { get; set; } = new MyModel();
}
The Model class contains the logic
public class MyModel : INotifyPropertyChanged
{
private string _value1;
public string Value1
{
get { return _value1; }
set
{
if (_value1 != value)
{
_value1 = value;
// Cause the updated value to be displayed on the UI
OnPropertyChanged(nameof(Value1));
// Is the entered value a number (int)?
int numericValue;
if (int.TryParse(value, out numericValue))
{
// It's a number so set the other value
// multiplied by the ratio
Value2 = (numericValue * 3).ToString();
}
else
{
// A number wasn't entered so indicate this
Value2 = "NaN";
}
// Cause the updated value2 to be displayed
OnPropertyChanged(nameof(Value2));
}
}
}
// We can use the automatic property here as don't need any logic
// relating the getting or setting this property
public string Value2 { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
With the above, when a number is entered for Value1 then Value2 will show a number three times as much (because I've set the ratio of 3).
You may notice that if you try the above that the change doesn't happen immediately and Value2 is only updated when the focus leaves the Value1 text box. This is because, by default, the two-way binding is only updated when focus is lost. This can easily be changed though.
If instead of using the new x:Bind method of binding we use the traditional Binding method we can force the binding to be updated whenever we want. Say, when the text is changed.
Modify the TextBox declaration like this:
<TextBox Text="{Binding ViewModel.Value1, Mode=TwoWay}"
TextChanged="TextBox_OnTextChanged" />
Note that the binding syntax is different and we've added an event.
The handler of the event is
private void TextBox_OnTextChanged(object sender, TextChangedEventArgs e)
{
var be = (sender as TextBox).GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();
}
This forces the binding to update but there's another change we must make as well.
With the x:Bind syntax it tries to bind to the page. With the older Binding syntax it binds to the DataContext of the page. To make these the same, update the page constructor like this
public MainPage()
{
this.InitializeComponent();
this.DataContext = this;
}
Now the app will work again and Value2 will be updated after every key press in the Value1 text box.
I have in my Xaml a pivot control :
<controls:Pivot ItemsSource="{Binding ObjectList}">
<controls:Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock />
</DataTemplate>
</controls:Pivot.HeaderTemplate>
<controls:Pivot.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Value1}" />
<TextBlock Text="{Binding Value2}" />
</StackPanel>
</DataTemplate>
</controls:Pivot.ItemTemplate>
</controls:Pivot>
My ViewModel is :
public class MyObject
{
public string Value1 { get; set; }
public string Value2 { get; set; }
}
public class MyViewModel : ViewModelBase
{
public const string ObjectListPropertyName = "ObjectList";
private ObservableCollection<MyObject> _objectList;
public ObservableCollection<MyObject> ObjectList
{
get
{
return _objectList;
}
private set
{
if (_objectList == value)
return;
_objectList = value;
RaisePropertyChanged(ObjectListPropertyName);
}
}
private DispatcherTimer timer;
public MyViewModel()
{
ObservableCollection<MyObject> collection = new ObservableCollection<MyObject>
{
new MyObject {Value1 = "One"},
new MyObject {Value1 = "Two"},
new MyObject {Value1 = "Tree"}
};
ObjectList = collection;
timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2)};
timer.Tick += timer_Tick;
timer.Start();
}
void timer_Tick(object sender, EventArgs e)
{
foreach (MyObject myObject in _objectList)
{
myObject.Value2 = "Something";
}
Application.Current.RootVisual.Dispatcher.BeginInvoke( () => RaisePropertyChanged(ObjectListPropertyName));
}
}
When the timer_tick is reached, I supposed the pivot control to refresh with the new values ... but I can't see any changes.
What do I miss ?
Thanks in advance for your help
I'm guessing that possibly updating the members of the list without updating the list itself is the problem. When you raise the property changed event - it is for the entire collection. The collection is still pointing to an equal reference of itself, despite the fact that the members have changed.
Try placing a breakpoint in the setter and see if the property changed event is fired.