Bind data collection on xamarin using mvvm - c#

So I started learning xamarin and trying different approaches about data binding from the view to the model. I'm retrieving data from a service using post request and after I get the data I can't bind them to the view. After a lot of research I found some interesting solutions and I tried different approaches.
This what I have achieved until now :
My view :
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Ideas.Pages.IdeasSinglePage"
xmlns:vm="clr-namespace:Ideas.ViewModel;assembly=Ideas"
Title="My idea">
<ContentPage.BindingContext>
<vm:IdeasViewModel/>
</ContentPage.BindingContext>
<StackLayout>
<Button Command="{Binding GetIdeasCommand}"
Text="Bileta Ime"
TextColor="White"
FontSize="15"
BackgroundColor="#29abe2"/>
<Label Text="test"></Label>
<ListView ItemsSource="{Binding Ideas}"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Padding="20, 10">
<Label Text="{Binding IdeasName}"
FontSize="16"
TextColor="RoyalBlue"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
This is my viewmodel
public class IdeasViewModel : INotifyPropertyChanged
{
ApiServices _apiServices = new ApiServices();
public List<Ideas> Ideas
{
get { return Ideas; }
set
{
Ideas= value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public ICommand GetIdeasCommand
{
get
{
return new Command(async () =>
{
Ideas= await _apiServices.GetIdeasAsync();
});
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
This is my service :
public async Task<List<Ideas>> GetIdeasAsync()
{
ListIdeasDetails ideas= null;
try {
var client = new HttpClient();
client.DefaultRequestHeaders.Add("parameter", "parameter");
client.DefaultRequestHeaders.Add("parameter", parameter);
HttpContent content = new StringContent("");
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
var response = await client.PostAsync("https://heregoes/themethod", content);
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync();
ideas= JsonConvert.DeserializeObject<ListIdeasDetails>(json);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message.ToString());
}
return ideas.Ideas;
}
}
And here are my two models :
public class Ideas
{
public string IdeasID { get; set; }
public string IdeasName { get; set; }
}
class ListIdeasDetails
{
public List<Ideas> Ideas{ get; set; }
public string ExceptionMessage { get; set; }
public bool HasException { get; set; }
}
I would really appreciate some help! Thanks!!

Seems like you have a problem with your property definition:
public List<Ideas> Ideas
{
get { return Ideas; }
set
{
Ideas= value; // Endless loop
OnPropertyChanged();
}
}
Add a backing field this way:
List<Ideas> _ideas;
public List<Ideas> Ideas
{
get { return _ideas; }
set
{
if(value == _ideas) return;
_ideas = value;
OnPropertyChanged();
}
}
I would recommend to use Fody.PropertyChanged in order to reduce the amount of boiler-plate code related to INotifyPropertyChanged. Using Fody your ViewModel will look as simple as:
public class IdeasViewModel : INotifyPropertyChanged
{
ApiServices _apiServices = new ApiServices();
public List<Ideas> Ideas { get;set; }
public event PropertyChangedEventHandler PropertyChanged;
public ICommand GetIdeasCommand
{
get
{
return new Command(async () =>
{
Ideas= await _apiServices.GetIdeasAsync();
});
}
}
}
P.S.: Beside your main question I would like to point out the next things that does not look good in your code.
Using POST to download data from WEB.
Class name 'ApiServices'.
You can let me know if you need further assistance by commenting.

Related

Why this picker is not selecting an item as expected?

I'm trying to do a picker that loads ItemSource from a List and depending on an external event, change its SelectedIndex based on Local.id, but what I've been trying so far didn't works.
C# code:
public class Local
{
public string cidade { get; set; }
public int id { get; set; }
}
public int CidadeSelectedIndex{ get; set; }
string jsonCidades;
public async void CarregaCidades()
{
try
{
using (WebClient browser = new WebClient())
{
Uri uriCidades = new Uri("xxxxxxx.php");
jsonCidades = await browser.DownloadStringTaskAsync(uriCidades);
}
var ListaCidades = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Local>>(jsonCidades);
PickerCidades.ItemsSource = ListaCidades;
}
catch (Exception)
{
throw;
}
}
//In some moment of the execution, this code is called:
Local localizacao = JsonConvert.DeserializeObject<Local>(json);
if (localizacao.GetType().GetProperty("id") != null)
{
/*CidadeSelectedItem = localizacao;
I tried that before with SelectedItem="{Binding CidadeSelectedItem, Mode=TwoWay}" */
CidadeSelectedIndex = localizacao.id; // now trying this
}
Before I was trying to bind using ItemDisplayBinding="{Binding ListaCidades.cidade, Mode=OneWay}" but since it was not working I start to use ItemSources=ListaCidades
My XAML code:
<Picker x:Name="PickerCidades"
SelectedIndex="{Binding CidadeSelectedIndex, Mode=TwoWay}"
Grid.Column="1" Grid.Row="0"
SelectedIndexChanged="PickerCidades_SelectedIndexChanged">
</Picker>
I think it's not working because I'm setting the items using ItemsSource. I think I need to bind it using xaml. Would be nice have some help.
Do you want to achieve the result like following GIF?
My xaml layout like following code.
<StackLayout>
<!-- Place new controls here -->
<Picker x:Name="PickerCidades"
ItemsSource="{ Binding locals}"
SelectedIndex="{Binding CidadeSelectedIndex, Mode=TwoWay}"
ItemDisplayBinding="{Binding cidade}"
Grid.Column="1" Grid.Row="0"
SelectedIndexChanged="PickerCidades_SelectedIndexChanged">
</Picker>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Text="CidadeSelectedIndex: " Grid.Column="0" Grid.Row="0"/>
<Label Text="{Binding CidadeSelectedIndex}" Grid.Column="1" Grid.Row="0"/>
</Grid>
</StackLayout>
Layout background code.
public partial class MainPage : ContentPage
{
MyViewModel myViewModel;
public MainPage()
{
InitializeComponent();
myViewModel= new MyViewModel();
BindingContext = myViewModel;
}
private void PickerCidades_SelectedIndexChanged(object sender, EventArgs e)
{
var picker = (Picker)sender;
int selectedIndex = picker.SelectedIndex;
myViewModel.CidadeSelectedIndex = selectedIndex;
}
}
MyViewMode code.I use static data for testing. You can achieve the INotifyPropertyChanged interface to change dynamically.
public class MyViewModel : INotifyPropertyChanged
{
int _cidadeSelectedIndex=1;
public int CidadeSelectedIndex
{
set
{
if (_cidadeSelectedIndex != value)
{
_cidadeSelectedIndex = value;
OnPropertyChanged("CidadeSelectedIndex");
}
}
get
{
return _cidadeSelectedIndex;
}
}
public ObservableCollection<Local> locals { get; set; }
public MyViewModel()
{
locals = new ObservableCollection<Local>();
locals.Add(new Local() { cidade= "xxx0" , id= 0 });
locals.Add(new Local() { cidade = "xxx1", id = 1 });
locals.Add(new Local() { cidade = "xxx2", id = 2 });
locals.Add(new Local() { cidade = "xxx3", id = 3 });
locals.Add(new Local() { cidade = "xxx4", id = 4 });
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
If the goal is to change the User Interface from code you need to have a ViewModel that implements INotifyPropertyChanged (or inherits from a base that does). Then instead of SelectedIndex bound property being a simple get; set as below it fires off the PropertyChanged event.
public int CidadeSelectedIndex{ get; set; }
Needs to fire notification event. Something along these lines
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private int _cidadeSelectedIndex;
public int CidadeSelectedIndex
{
get => _cidadeSelectedIndex;
set {
_cidadeSelectedIndex = value;
NotifyPropertyChanged();
}
}
}

Binding Issue on child view in Xamarin

I have a shared view for Add and Detail page. For some reason in the detail page, the view model won't binding to this child view (page come up blank as in NO populated value from the api service). Any ideas?
Debug this and there was a data coming from web api for both CategoryList as well as _activity.
How to debug this binding process?
ActivityView.xaml
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="AthlosifyMobileApp.Views.ActivityView">
<StackLayout Spacing="12">
<Entry x:Name="txtName" Text="{Binding Name}" HeightRequest="40" BackgroundColor="White" Placeholder="Name" HorizontalOptions="FillAndExpand"/>
<Entry x:Name="txtNoOfMinutes" Keyboard="Numeric" Text="{Binding NoOfMinutes}" BackgroundColor="White" Placeholder="NoOfMinutes" HorizontalOptions="FillAndExpand"/>
<Entry x:Name="txtDescription" Text="{Binding Description}" HeightRequest="40" BackgroundColor="White" Placeholder="Description" HorizontalOptions="FillAndExpand"/>
<Picker ItemsSource="{Binding CategoryList}" ItemDisplayBinding="{Binding Name}" SelectedItem="{Binding SelectedCategory}"></Picker>
</StackLayout>
</ContentView>
ActivityView.xaml.cs
namespace AthlosifyMobileApp.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ActivityView : ContentView
{
public ActivityView ()
{
InitializeComponent ();
}
}
}
ActivityDetailViewModel.cs
namespace AthlosifyMobileApp.ViewModels
{
public class ActivityDetailViewModel : ActivityBaseViewModel
{
public ICommand DeleteCommand { get; private set; }
public ICommand UpdateCommand { get; private set; }
public ActivityDetailViewModel(INavigation navigation, int selectedActivityId)
{
_navigation = navigation;
_activityValidator = new ActivityValidator();
_activity = new Activity();
_activity.Id = selectedActivityId;
_apiService = new ApiService();
//DeleteCommand = new Command(async () => await HandleDeleteActivity());
UpdateCommand = new Command(async () => await UpdateActivity());
FetchActivityDetail();
FetchCategories();
}
async void FetchActivityDetail()
{
_activity = await _apiService.GetActivity(_activity.Id);
}
async void FetchCategories()
{
CategoryResult categoryResult = await _apiService.GetCategories();
CategoryList = categoryResult.Results;
}
async Task UpdateActivity()
{
_activity.OwnerId = Preferences.Get(Constant.Setting_UserId, "");
_activity.CategoryId = SelectedCategory.Id;
_activity.CategoryName = SelectedCategory.Name;
var validationResults = _activityValidator.Validate(_activity);
if (validationResults.IsValid)
{
bool isUserAccept = await Application.Current.MainPage.DisplayAlert("Contact Details", "Update Contact Details", "OK", "Cancel");
if (isUserAccept)
{
var response = await _apiService.UpdateActivity(_activity.Id,_activity);
if (!response)
{
await Application.Current.MainPage.DisplayAlert("Add Activity", "Error", "Alright");
}
else
{
await _navigation.PushAsync(new ActivityListPage());
}
await _navigation.PopAsync();
}
}
else
{
await Application.Current.MainPage.DisplayAlert("Add Contact", validationResults.Errors[0].ErrorMessage, "Ok");
}
}
public async Task HandleDeleteActivity(int id)
{
var alert = await Application.Current.MainPage.DisplayAlert("Warning", "Do you want to delete this item?", "Yes", "Cancel");
if (alert)
{
var response = await _apiService.DeleteActivity(id);
if (!response)
{
await Application.Current.MainPage.DisplayAlert("Error", "Something wrong", "Alright");
}
else
{
await _navigation.PushAsync(new ActivityListPage());
}
}
}
}
}
ActivityBaseViewModel.cs
namespace AthlosifyMobileApp.ViewModels
{
public class ActivityBaseViewModel : INotifyPropertyChanged
{
public Activity _activity;
public INavigation _navigation;
public IValidator _activityValidator;
public ApiService _apiService;
public string Name
{
get
{
return _activity.Name;
}
set
{
_activity.Name = value;
NotifyPropertyChanged("Name");
}
}
public string Description
{
get { return _activity.Description; }
set
{
_activity.Description = value;
NotifyPropertyChanged("Description");
}
}
public int NoOfMinutes
{
get { return _activity.NoOfMinutes; }
set
{
_activity.NoOfMinutes = value;
NotifyPropertyChanged("NoOfMinutes");
}
}
public int CategoryId
{
get { return _activity.CategoryId; }
set
{
_activity.CategoryId = value;
NotifyPropertyChanged("CategoryId");
}
}
public string CategoryName
{
get { return _activity.CategoryName; }
set
{
_activity.CategoryName = value;
NotifyPropertyChanged("CategoryName");
}
}
//List<Activity> _activityList;
InfiniteScrollCollection<Activity> _activityList;
//public List<Activity> ActivityList
public InfiniteScrollCollection<Activity> ActivityList
{
get => _activityList;
set
{
_activityList = value;
NotifyPropertyChanged("ActivityList");
}
}
List<Category> _categoryList;
public List<Category> CategoryList
{
get { return _categoryList; }
set
{
_categoryList = value;
NotifyPropertyChanged("CategoryList");
}
}
public Category SelectedCategory
{
get
{
return _activity.SelectedCategory;
}
set
{
_activity.SelectedCategory = value;
NotifyPropertyChanged("SelectedCategory");
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
ActivityDetailPage.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:AthlosifyMobileApp.Views"
x:Class="AthlosifyMobileApp.Views.ActivityDetailPage"
Title="Detail Activity">
<ContentPage.ToolbarItems>
<ToolbarItem Command="">
<ToolbarItem.IconImageSource>
<FontImageSource Glyph="" FontFamily="{StaticResource MaterialFontFamily}"/>
</ToolbarItem.IconImageSource>
</ToolbarItem>
<ToolbarItem Command="{Binding UpdateCommand}">
<ToolbarItem.IconImageSource>
<FontImageSource Size="30" Glyph="" FontFamily="{StaticResource MaterialFontFamily}"/>
</ToolbarItem.IconImageSource>
</ToolbarItem>
</ContentPage.ToolbarItems>
<ContentPage.Content>
<StackLayout Padding="20" Spacing="12">
<local:ActivityView />
</StackLayout>
</ContentPage.Content>
</ContentPage>
ActivityDetailPage.xaml.cs
namespace AthlosifyMobileApp.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ActivityDetailPage : ContentPage
{
public ActivityDetailPage(int activityId)
{
InitializeComponent ();
BindingContext = new ActivityDetailViewModel(Navigation, activityId);
}
}
}
Based on the code you shared, I think it's likely you're not seeing any data on the activity detail page because you are fetching the data via an async method that is not awaited (FetchActivityData). Btw, async void method should be avoided if possible. There is no way to catch/handle exceptions thrown from them.
It looks like you are not awaiting because you are calling from the constructor of your viewmodel. What actually happens here is that the constructor returns immediately, while FetchActivityDetail() and FetchCategories() continue to run in the background. The page is displayed, but there is no data yet, so you don't see anything displayed. Then, when FetchActivityDetail completes, it sets _activity, but that's a field, so no PropertyChanged events are fired, so the page doesn't know it needs to update.
Here are a few suggestions:
Do not perform long-running processes (like fetching data) in constructors. Passing in the existing data (like your activityid), is generally ok, although it can make using dependency injection a bit harder, if you eventually want to do that.
When navigating to a viewmodel that requires fetching data, I generally recommend waiting until the view/vm are displayed before making the api call. To do this, I have all my views call an OnAppearing method in my viewmodels. This is easily moved into a BasePage and BaseViewModel that everything inherits from. Then, you can do things like setting an IsBusy status (to trigger some UI like a spinner), and populate your data. It could look something like this:
public override async Task OnAppearing()
{
await base.OnAppearing();
try
{
IsBusy = true;
await FetchActivityDetail();
await FetchCategories();
}
catch (Exception ex)
{
//handle/display error
}
finally
{
IsBusy = false;
}
}
Another option would be to make this a method that's called prior to navigation, but that would require creating the viewmodel first, which is a different navigation pattern than you're using here. There are some good examples out there of viewmodel-first navigation, but I won't go into that here.
Ensure that when data is fetched, it sets properties that cause PropertyChanged events to fire, so the view bindings update. You can't just set a backing field.
According to your description, you want to bind custom view in Xamarin.Forms, I suggest you don't assign binding internally inside custom controls, use this:
<ContentView
x:Class="demo2.simplecontrol.View1"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<ContentView.Content>
<StackLayout>
<Entry x:Name="label1" />
<Entry x:Name="label2" />
</StackLayout>
</ContentView.Content>
public partial class View1 : ContentView
{
public View1 ()
{
InitializeComponent ();
}
public static readonly BindableProperty Label1Property= BindableProperty.Create(
nameof(Label1),
typeof(string),
typeof(View1),
"",
BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (newValue != null && bindable is View1 control)
{
var actualNewValue = (string)newValue;
control.label1.Text = actualNewValue;
}
});
public string Label1 { get; set; }
public static readonly BindableProperty Label2Property = BindableProperty.Create(
nameof(Label2),
typeof(string),
typeof(View1),
"",
BindingMode.TwoWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (newValue != null && bindable is View1 control)
{
var actualNewValue = (string)newValue;
control.label2.Text = actualNewValue;
}
});
public string Label2 { get; set; }
}
Then you can use this custom view in ContentPage.
<ContentPage
x:Class="demo2.simplecontrol.Page10"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:demo2.simplecontrol">
<ContentPage.Content>
<StackLayout>
<Label
HorizontalOptions="CenterAndExpand"
Text="Welcome to Xamarin.Forms!"
VerticalOptions="CenterAndExpand" />
<local:View1 Label1="{Binding text1}" Label2="{Binding text2}" />
</StackLayout>
</ContentPage.Content>
public partial class Page10 : ContentPage, INotifyPropertyChanged
{
private string _text1;
public string text1
{
get { return _text1; }
set
{
_text1 = value;
RaisePropertyChanged("text1");
}
}
private string _text2;
public string text2
{
get { return _text2; }
set
{
_text2 = value;
RaisePropertyChanged("text2");
}
}
public Page10 ()
{
InitializeComponent ();
text1 = "test1";
text2 = "test2";
this.BindingContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Finally, you get data coming from web api for CategoryList, so you can add break point to check if having data.
'I'm not sure, but apparently the page is changing the binding context of your view.
Set a breakpoint inside your OnBindingContextChanged overridden method of your view and debug it. If it is confirmed, instantiate your view model from your page instead.

INotifyPropertyChanged doesn't work

I have a simple object (which is globally initiated in App.xaml.cs):
public class now_playing : INotifyPropertyChanged
{
// notify
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string p)
{
Debug.WriteLine(p + ": notify propertychanged");
PropertyChangedEventHandler handler = PropertyChanged;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(p));
}
// artist
public string artist
{
get
{
return _artist;
}
set
{
_artist = value;
NotifyPropertyChanged("artist");
}
}
private string _artist;
// album
public string album
{
get
{
return _album;
}
set
{
_album = value;
NotifyPropertyChanged("album");
}
}
private string _album;
// track title
public string tracktitle
{
get
{
return _tracktitle;
}
set
{
_tracktitle = value;
NotifyPropertyChanged("tracktitle");
}
}
private string _tracktitle;
}
Whenever I change the values, the class does notify (I see the debug).
So I guess the problems lies in my XAML or the code behind.
Page code:
public sealed partial class nowplaying : Page
{
// artistdata
public string artist { get { return App.nowplaying.artist; } }
// albumdata
public string album { get { return App.nowplaying.album; } }
// trackdata
public string tracktitle { get { return App.nowplaying.tracktitle; } }
public nowplaying()
{
this.InitializeComponent();
this.DataContext = this;
}
}
XAML:
<Grid Margin="50">
<TextBlock Text="{Binding tracktitle}" Foreground="White" FontSize="40"/>
<TextBlock Foreground="#dcdcdc" FontSize="20" Margin="0,50,0,0">
<Run Text="{Binding artist}"/>
<Run Text=" - "/>
<Run Text="{Binding album}"/>
</TextBlock>
</Grid>
Why does the UI not update when I change values?
Stack trace:
Music.exe!Music.App.InitializeComponent.AnonymousMethod__6(object sender = {Music.App}, Windows.UI.Xaml.UnhandledExceptionEventArgs e = {Windows.UI.Xaml.UnhandledExceptionEventArgs}) Line 50 C#
Music.exe!play_music.MessageReceivedFromBackground(object sender = null, Windows.Media.Playback.MediaPlayerDataReceivedEventArgs e = {Windows.Media.Playback.MediaPlayerDataReceivedEventArgs}) Line 57 C#
UPDATE: problem solved! I had to use a dispatcher when calling the propertychanged event:
CoreDispatcher dispatcher = CoreWindow.GetForCurrentThread().Dispatcher;
if (PropertyChanged != null)
{
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
this.PropertyChanged(this, new PropertyChangedEventArgs(p));
});
}
You "loose" the change notification in the properties in the Page as these properties do not have any change notifiaction.
Try using now_playing directly:
public sealed partial class nowplaying : Page
{
public now_playing NowPlaying { get { return App.nowplaying; } }
public nowplaying()
{
this.InitializeComponent();
this.DataContext = this;
}
}
and
<Run Text="{Binding NowPlaying.artist}"/>
Otherwise you need to implement INotifiyPropertyChanged in nowplaying and forward the events from now_playing.
You actually binding to artist, album and tracktitle Of nowplaying class which does implement INotifyPropertyChanged

Creating Text boxes issue?

I have posted a previous question but there was little help so I tried to start coding it and looking up some more on my own and I'm stuck on some code. I'm trying to follow MVVM
I have created a Class called Standard which looks like this:
namespace MVVModel
{
public class Standard
{
string _title;
string _question;
public string Title
{
get { return _title; }
set { _title = value; }
}
public string Question
{
get { return _question; }
set { _question = value; }
}
}
}
Then I created ViewModel class which looks like this:
namespace MVVModel
{
class ViewModel
{
ObservableCollection<Standard> _title = new ObservableCollection<Standard>();
ObservableCollection<Standard> _question = new ObservableCollection<Standard>();
public ViewModel()
{
}
public ObservableCollection<Standard> Title
{
get
{
return _title;
}
set
{
_title = value;
}
}
public ObservableCollection<Standard> Question
{
get
{
return _question;
}
set
{
_question = value;
}
}
}
}
Here is my XAML:
<Grid>
<Button x:Name="btnTitle" Content="Title" HorizontalAlignment="Left" Margin="691,22,0,0" VerticalAlignment="Top" Width="75"/>
<Button x:Name="btnQuestion" Content="Question" HorizontalAlignment="Left" Margin="797,22,0,0" VerticalAlignment="Top" Width="75" Command="{Binding AddTitle}"/>
<ItemsControl ItemsSource="{Binding Question}" Margin="0,86,0,0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
I just want to create a textbox dynamically but nothing it showing, any help?
I have mentioned in my previous answer about implementing INotifyPropertyChanged.
Why you again need a collection of Questions and Titles in your viewModel, this already exists in the Standard class.
You need a collection of Standard class in your main ViewModel. Thats what I get from your question if I have understood it correctly.
Here is the implementation for INotifyPropertyChanged
public class Standard : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// Create the OnPropertyChanged method to raise the event
protected void NotifyOfPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
protected void NotifyOfPropertyChanged<TProperty>(Expression<Func<TProperty> property)
{
NotifyOfPropertyChanged(property.GetMemberInfo().Name);
}
string _title;
ObservableCollection<string> _questions;
public string Title
{
get { return _title; }
set {
_title = value;
NotifyOfPropertyChanged(()=>Title);
}
}
public ObservableCollection<string> Questions
{
get { return _questions; }
set {
_questions = value;
NotifyOfPropertyChanged(()=>Questions);
}
}
}
You must follow a couple of steps to accomplish this task.
First you need to bind the collection of Standard to the Grid, in place of Question.
Second you need to bind a property of the previous class to the textbox.
Ex:
<DataTemplate>
<TextBox Text="{Binding Question"} />
</DataTemplate>
Edit:
I would like to mention this article to help you:
http://www.codeproject.com/Articles/165368/WPF-MVVM-Quick-Start-Tutorial

Binding to model that contains collections in MVVM

I am getting started with MVVM (using Caliburn.Micro) and have come across an issue which I'm not sure if I'm doing this correctly. I have a model MediaCacherConfig which represents a textfile that stores the data in json format. The model contains 2 lists of strings and one string by itself.
What I am struggling with is how to correctly set up the viewmodel and in particular the AddNewFolder() method. I'm not sure if I am raising the correct event and whether the viewmodel's representation is correct. I can see how to bind to a simple property, but binding to a collection seems a bit more of a head spinner as I am creating a whole new collection everytime an item (string) is added.
Furthermore, when I load an entirely new model I have to run the NotifyPropertyChanged() method on all the properties which doesn't make sense to me.
Any guidance is much appreciated.
public class MediaCacherConfig : IConfig
{
public string DatabaseFileName { get; set; }
public ICollection<string> FoldersToScan { get; set; }
public ICollection<string> ExtensionsToIgnore { get; set; }
}
I have a viewmodel MediaCacherConfigViewModel:
public class MediaCacherConfigViewModel : PropertyChangedBase
{
private MediaCacherConfig Model { get; set; }
public string DatabaseFileName
{
get { return Model.DatabaseFileName; }
set
{
Model.DatabaseFileName = value;
NotifyOfPropertyChange(() => DatabaseFileName);
}
}
public BindableCollection<string> FoldersToScan
{
get
{
return new BindableCollection<string>(Model.FoldersToScan);
}
set
{
Model.FoldersToScan = value;
NotifyOfPropertyChange(() => FoldersToScan);
}
}
public BindableCollection<string> ExtensionsToIgnore
{
get
{
return new BindableCollection<string>(Model.ExtensionsToIgnore);
}
set
{
Model.ExtensionsToIgnore = value;
NotifyOfPropertyChange(() => ExtensionsToIgnore);
}
}
/* Constructor */
public MediaCacherConfigViewModel()
{
LoadSampleConfig();
}
/* Methods */
public void LoadSampleConfig()
{
MediaCacherConfig c = new MediaCacherConfig();
string sampleDatabaseFileName = "testing.config";
List<string> sampleFoldersToScan = new List<string>();
sampleFoldersToScan.Add("A");
sampleFoldersToScan.Add("B");
sampleFoldersToScan.Add("C");
List<string> sampleExtensionsToIgnore = new List<string>();
sampleExtensionsToIgnore.Add("txt");
sampleExtensionsToIgnore.Add("mov");
sampleExtensionsToIgnore.Add("db");
sampleExtensionsToIgnore.Add("dat");
c.DatabaseFileName = sampleDatabaseFileName;
c.FoldersToScan = sampleFoldersToScan;
c.ExtensionsToIgnore = sampleExtensionsToIgnore;
Model = c;
NotifyOfPropertyChange(() => DatabaseFileName);
NotifyOfPropertyChange(() => FoldersToScan);
NotifyOfPropertyChange(() => ExtensionsToIgnore);
}
public void AddNewFolder()
{
Model.FoldersToScan.Add("new one added");
NotifyOfPropertyChange(() => FoldersToScan);
}
public void SaveConfig()
{
ConfigTools.Configure(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Cacher", "Config"));
ConfigTools.SaveConfig(Model,"sampleconfig.txt");
}
public void LoadConfig()
{
ConfigTools.Configure(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Cacher", "Config"));
MediaCacherConfig m = ConfigTools.LoadConfig<MediaCacherConfig>("sampleconfig.txt") as MediaCacherConfig;
Model = m;
NotifyOfPropertyChange(() => DatabaseFileName);
NotifyOfPropertyChange(() => FoldersToScan);
NotifyOfPropertyChange(() => ExtensionsToIgnore);
}
}
And here is my view:
<UserControl x:Class="MediaCacher.Views.MediaCacherConfigView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="413" Width="300">
<Grid MinWidth="300" MinHeight="300" Background="LightBlue" Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="409*"/>
<RowDefinition Height="4*"/>
</Grid.RowDefinitions>
<TextBox x:Name="DatabaseFileName" TextWrapping="Wrap" Margin="10,64,10,0" HorizontalAlignment="Center" Width="280" Height="42" VerticalAlignment="Top"/>
<ListBox x:Name="FoldersToScan" HorizontalAlignment="Left" Height="145" Margin="10,111,0,0" VerticalAlignment="Top" Width="280"/>
<ListBox x:Name="ExtensionsToIgnore" HorizontalAlignment="Left" Height="145" Margin="10,261,0,0" VerticalAlignment="Top" Width="280"/>
<Button x:Name="AddNewFolder" Content="Add" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="87" Height="49"/>
<Button x:Name="LoadConfig" Content="Load" HorizontalAlignment="Left" Margin="102,10,0,0" VerticalAlignment="Top" Width="96" Height="49"/>
<Button x:Name="SaveConfig" Content="Save" HorizontalAlignment="Left" Margin="203,10,0,0" VerticalAlignment="Top" Width="87" Height="49"/>
</Grid>
First, here you are returning a brand new collection every time, so obviously nothing gets persisted.
public BindableCollection<string> FoldersToScan
{
get
{
return new BindableCollection<string>(Model.FoldersToScan);
}
set
{
Model.FoldersToScan = value;
NotifyOfPropertyChange(() => FoldersToScan);
}
}
Secondly, your AddFolder method should belong in your ViewModel. When you Add a string to your already existing collection the fact that it is a BindingCollection should fire off an event to your View automatically that a new Item was added.
This is how I would do it. This is obviously an example for demonstration purposes, please add everything else you need. Youd ideall want to pass EventArgs and note I am not implementing INotifyPorpertyChanged because I don't have time to write it all out. Also I am using ObservableCollection but you can use your BindableCollection.
The point of this example is to show you how to manage your ViewModel - > Model communcation. Technically speaking your View -> ViewModel should talk through a CommandPattern.
public class YourViewModel
{
private readonly YourModel model;
private ObservableCollection<string> foldersToScan = new ObservableCollection<string>();
public ObservableCollection<string> FoldersToScan
{
get { return this.foldersToScan; }
}
public YourViewModel(YourModel model)
{
this.model = model;
this.model.OnItemAdded += item => this.foldersToScan.Add(item);
}
public void AddFolder(string addFolder) //gets called from view
{
this.model.AddFolder(addFolder); //could be ICommand using Command Pattern
}
}
public class YourModel
{
private readonly List<string> foldersToScan;
public IEnumerable<string> FoldersToScan
{
get { return this.foldersToScan; }
}
public event Action<string> OnItemAdded;
public YourModel()
{
this.foldersToScan = new List<string>();
}
public void AddFolder(string folder)
{
this.foldersToScan.Add(folder);
this.RaiseItemAdded(folder);
}
void RaiseItemAdded(string folder)
{
Action<string> handler = OnItemAdded;
if (handler != null) handler(folder);
}
}

Categories