I use MvvmCross with Xamarin Form.
Therefore, I use RaisePropertyChanged to notify View.
However, RaisePropertyChanged does not fire propertyChanged in ViewA.
I do not know where to start to debug or check local variables...
Flow
If I change Data.Value somewhere, flow is like below.
event Data.ValueChanged invoked.
ModelA.OnValueChanged calls OnPropertyChanged
ViewModelA.OnModelPropertyChanged calls RaisePropertyChanged
expect ViewA.OnChanged called, but fail...
XAML
I run and check if XAML binding is working.
<DataTemplate x:Key="ViewB">
<ViewB Data="{Binding Data}" />
</DataTemplate>
View
I defined BindableProperty as below.
// this class is abstract!
public abstract class ViewA : MvxContentView
{
public static readonly BindableProperty DataProperty =
BindableProperty.Create(
propertyName: "Property",
returnType: typeof(Data),
declaringType: typeof(ViewA),
defaultValue: null,
propertyChanged: OnChanged);
static void OnChanged(BindableObject bindable, object oldValue, object newValue)
{
if (newValue is null) { return; }
// some codes
}
}
// actual class
public partial class ViewB : ViewA
{
public ViewB()
{
InitializeComponent();
}
}
ViewModel
// this is also abstract!
public abstract class ViewModelA<T> : MvxViewModel<T>
{
protected T _model;
public Data Data
{
get => _model.Data;
}
public T Model
{
get => _Model;
set
{
if (SetProperty(ref _model, value))
{
// Register event handler
_model.PropertyChanged += OnModelPropertyChanged;
}
}
}
private void OnModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "DataChanged":
{
// I expect this will fire 'propertyChanged' of BindableProperty.
// But it is not fired...
RaisePropertyChanged(() => Data);
}
break;
}
}
}
// actual class
public class ViewModelB : ViewModelA<ModelA>
{
public ViewModelB() : base()
{
}
}
Model
public class LayerModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private Data _data;
public Data Data
{
get
{
return _data;
}
set
{
if (_data != value)
{
_data = value;
_data.ValueChanged += OnValueChanged;
OnPropertyChanged("DataChanged");
}
}
}
private void OnValueChanged(object sender, EventArgs e)
{
OnPropertyChanged(""DataChanged"");
}
}
Data
public class Data
{
private int _value;
public int Value
{
get => _value;
set
{
if(_value != value)
{
// 2020.07.06 Edited
var evetArg = new DataChangedArgs
{
OldData = _value;
NewData = value;
};
_value = value;
ValueChanged?.Invoke(this, evetArg);
}
}
}
public event EventHandler ValueChanged;
}
2020.07.06 Added
public class DataChangedArgs : EventArgs
{
public int OldData { get; set; }
public int NewData { get; set; }
}
I suggest you to simplify some classes. You created a custom event named ValueChanged, its basically the same INotifyPropertyChanged. It's recommend you to use the interface, there are a class MvxNotifyPropertyChanged (in MvvmCross) that implement the previous interface.
Here is an approximation (This ViewModel does not inherit from MvxViewModel but as I said, it's an approximation).
***EDIT Question update: The class Data can't be modified.
You are trying to bind Data with a BindableProperty, in this case the Data should implement the INotifyPropertyChanged to notify the changes. I recommend you to read about BindableObject.
In this case I propose the following solution:
Create a class inheriting from Data: it will implement the INotifyPropertyChanged to notify the value changes.
The structure should look similar to:
...
public class MainPageViewModel : MvxNotifyPropertyChanged
{
public Data Data => _model?.Data;
private LayerModel _model;
public LayerModel Model
{
get => _model;
set
{
SetProperty(ref _model, value, () =>
{
RaisePropertyChanged(nameof(Data));
});
}
}
}
public class LayerModel : MvxNotifyPropertyChanged
{
private Data _data;
public Data Data
{
get => _data;
set => SetProperty(ref _data, value);
}
}
public class Data
{
private int _value;
public int Value
{
get => _value;
set
{
if (_value == value)
return;
var eventArgs = new DataChangedEventArgs(_value, value);
_value = value;
ValueChanged?.Invoke(this, eventArgs);
}
}
public event EventHandler<EventArgs> ValueChanged;
}
public class DataChangedEventArgs : EventArgs
{
public DataChangedEventArgs(int oldData, int newData)
{
OldData = oldData;
NewData = newData;
}
public int OldData { get; }
public int NewData { get; }
}
public class NotificationData : Data, INotifyPropertyChanged
{
public static NotificationData FromData(Data data)
{
return new NotificationData {Value = data.Value};
}
public NotificationData()
{
ValueChanged += delegate
{
OnPropertyChanged(nameof(Value));
};
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
...
The following is an demo with an abstract view, and a child view consuming the MainPageViewModel.
The abstract view should look similar to
public abstract class ViewA : ContentPage
{
public Data ModelA
{
get => (Data)GetValue(ModelAProperty);
set => SetValue(ModelAProperty, value);
}
public static readonly BindableProperty ModelAProperty =
BindableProperty.Create(
propertyName: nameof(ModelA),
returnType: typeof(Data),
declaringType: typeof(ViewA),
defaultValue: null,
propertyChanged: OnChanged);
static void OnChanged(BindableObject bindable, object oldValue, object newValue)
{
if (newValue is null) { return; }
// some codes
}
}
The child view.
Code behind:
...
public partial class MainPage : ViewA
{
private int _counter = 10;
private MainPageViewModel ViewModel => (MainPageViewModel) BindingContext;
public MainPage()
{
InitializeComponent();
//--example initialization
var data = new Data
{
Value = _counter
};
ViewModel.Model = new LayerModel
{
Data = NotificationData.FromData(data)
};
}
private void Button_OnClicked(object sender, EventArgs e)
{
ViewModel.Model.Data.Value = ++_counter;
}
}
...
XAML:
<?xml version="1.0" encoding="utf-8" ?>
<xamstack:ViewA xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:xamstack="clr-namespace:xamstack;assembly=xamstack"
mc:Ignorable="d"
x:Class="xamstack.MainPage"
ModelA="{Binding Data}">
<ContentPage.BindingContext>
<xamstack:MainPageViewModel/>
</ContentPage.BindingContext>
<StackLayout Padding="20">
<Button Text="Increment" Clicked="Button_OnClicked"/>
<Label>
<Label.FormattedText>
<FormattedString>
<Span Text="Value: "/>
<Span Text="{Binding Path=ModelA.Value, Mode=OneWay, Source={RelativeSource AncestorType={x:Type ContentPage}}}"
TextColor="Red"
/>
</FormattedString>
</Label.FormattedText>
</Label>
</StackLayout>
</xamstack:ViewA>
I hope it match your case.
Related
how to get the value of the textbox and try to use it as int with binding?
<TextBox Text="{Binding SelectedAmount}"/>
I have tried like this, but the value of the binding is 0
public string SelectedAmount
{
get { return _selectedAmount; }
set { _selectedAmount = value; }
}
That is my main class, but the valau of the textbox stay 0, it does´t change
public partial class MainWindow : Window
{
int deposit;
int weeks;
int total;
public MainWindow()
{
InitializeComponent();
this.DataContext = new MyClass();
}
public class MyClass : 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 = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public int _selectedAmount;
public int SelectedAmount
{
get
{
return this._selectedAmount;
}
set
{
if (value != this._selectedAmount)
{
this._selectedAmount = value;
NotifyPropertyChanged();
}
}
}
}
public void BtnCompute_Click_1(object sender, RoutedEventArgs e)
{
MyClass ff = new MyClass();
int cc = ff.SelectedAmount;
deposit = cc;
}
}
}
You can bind Text to int with no effort.
When using bindings, you should either derive the class containing bindable properties from the interface INotifyPropertyChanged or the class DependencyObject. otherwise the binding will show only the default (initial) values.
public class MyClass : 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 = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public int _selectedAmount;
public int SelectedAmount
{
get
{
return this._selectedAmount;
}
set
{
if (value != this._selectedAmount)
{
this._selectedAmount = value;
NotifyPropertyChanged();
}
}
}
}
as in here
or
public class MyClass : DependencyObject
{
/// <summary>
/// Gets or Sets SelectedAmount Dependency Property
/// </summary>
public int SelectedAmount
{
get { return (int)GetValue(SelectedAmountProperty); }
set { SetValue(SelectedAmount Property, value); }
}
public static readonly DependencyProperty SelectedAmountProperty =
DependencyProperty.Register("SelectedAmount ", typeof(int), typeof(MyClass), new PropertyMetadata(0));
}
also do not forget to set the DataContext of your view.
//in view's constructor:
this.DataContext = new MyClass();
or
<UserControl>
<UserControl.DataContext>
<vm:MyClass/>
</UserControl.DataContext>
</UserControl>
Simply use like this,
public void BtnCompute_Click_1(object sender, RoutedEventArgs e)
{
MyClass ff = new MyClass();
int amount;
int.TryParse(ff.SelectedAmount, out amount);
deposit = amount;
}
i have updated a list item of a list.The item is successfully updated at source i.e database ,but the list is not getting updated with updated item.I have used INotifyPropertyChanged interface for the list items and the list is binded to an observable collection.
private tbl_Model _modelItem;
public tbl_Model ModelItem
{
get { return _modelItem; }
private set
{
_modelItem = value;
NotifyPropertyChanged("ModelItem");
}
}
private ObservableCollection<tbl_Model> _modelCollection;
public ObservableCollection<tbl_Model> ModelCollection
{
get { return _modelCollection; }
private set
{
_modelCollection = value;
NotifyPropertyChanged("ModelCollection");
}
}
public void btn_update()
{
//Code to update at database
//what should i write here to update the list ?
}
As you can see in image,the list shows model no. as 101 even after i updated it to 102
Thanks in advance
The auto generated model via Linq to Sql
public partial class tbl_Model : INotifyPropertyChanging, INotifyPropertyChanged
{
private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty);
private int _ID;
private string _Model_No;
private string _Name;
private string _Manufacturer;
private int _IsDelete;
#region Extensibility Method Definitions
partial void OnLoaded();
partial void OnValidate(System.Data.Linq.ChangeAction action);
partial void OnCreated();
partial void OnIDChanging(int value);
partial void OnIDChanged();
partial void OnModel_NoChanging(string value);
partial void OnModel_NoChanged();
partial void OnNameChanging(string value);
partial void OnNameChanged();
partial void OnManufacturerChanging(string value);
partial void OnManufacturerChanged();
partial void OnIsDeleteChanging(int value);
partial void OnIsDeleteChanged();
#endregion
public tbl_Model()
{
OnCreated();
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ID", AutoSync=AutoSync.OnInsert, DbType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)]
public int ID
{
get
{
return this._ID;
}
set
{
if ((this._ID != value))
{
this.OnIDChanging(value);
this.SendPropertyChanging();
this._ID = value;
this.SendPropertyChanged("ID");
this.OnIDChanged();
}
}
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Model_No", DbType="VarChar(50) NOT NULL", CanBeNull=false)]
public string Model_No
{
get
{
return this._Model_No;
}
set
{
if ((this._Model_No != value))
{
this.OnModel_NoChanging(value);
this.SendPropertyChanging();
this._Model_No = value;
this.SendPropertyChanged("Model_No");
this.OnModel_NoChanged();
}
}
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Name", DbType="VarChar(50) NOT NULL", CanBeNull=false)]
public string Name
{
get
{
return this._Name;
}
set
{
if ((this._Name != value))
{
this.OnNameChanging(value);
this.SendPropertyChanging();
this._Name = value;
this.SendPropertyChanged("Name");
this.OnNameChanged();
}
}
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Manufacturer", DbType="VarChar(50) NOT NULL", CanBeNull=false)]
public string Manufacturer
{
get
{
return this._Manufacturer;
}
set
{
if ((this._Manufacturer != value))
{
this.OnManufacturerChanging(value);
this.SendPropertyChanging();
this._Manufacturer = value;
this.SendPropertyChanged("Manufacturer");
this.OnManufacturerChanged();
}
}
}
[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_IsDelete", DbType="Int NOT NULL")]
public int IsDelete
{
get
{
return this._IsDelete;
}
set
{
if ((this._IsDelete != value))
{
this.OnIsDeleteChanging(value);
this.SendPropertyChanging();
this._IsDelete = value;
this.SendPropertyChanged("IsDelete");
this.OnIsDeleteChanged();
}
}
}
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void SendPropertyChanging()
{
if ((this.PropertyChanging != null))
{
this.PropertyChanging(this, emptyChangingEventArgs);
}
}
protected virtual void SendPropertyChanged(String propertyName)
{
if ((this.PropertyChanged != null))
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The ListView XAML Code
<ListView ItemsSource="{Binding ModelCollection,Mode=TwoWay}" SelectedItem="{Binding SelectedModelItem}" Style="{StaticResource viewinglist}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Model_No, Mode=TwoWay}" Header="Model No." Width="100"/>
<GridViewColumn DisplayMemberBinding="{Binding Name, Mode=TwoWay}" Header="Model Name" Width="200"/>
<GridViewColumn DisplayMemberBinding="{Binding Manufacturer, Mode=TwoWay}" Header="Manufacturer" Width="200"/>
</GridView>
</ListView.View>
</ListView>
Solution for anyone who sees the post:
This is where i was doing wrong-:
public tbl_Model SelectedModelItem {get; set;}
//on clicking edit this is what i used to do
ModelItem.ID = SelectedModelItem.ID;
ModelItem.Model_No = SelectedModelItem.Model_No;
ModelItem.Name = SelectedModelItem.Name;
ModelItem.Manufacturer = SelectedModelItem.Manufacturer;
The right way :
private tbl_Model _selectedModelItem;
public tbl_Model SelectedModelItem
{
get { return _selectedModelItem; }
set
{
_selectedModelItem = value;
NotifyPropertyChanged("SelectedModelItem");
}
}
on clicking edit
ModelItem = SelectedModelItem;
are you really sure that you update a ModelItem from WITHIN your Collection? you did not post the code.
The bindings and INotifyPropertyChanged implementation looks well.
<ListView ItemsSource="{Binding ModelCollection,Mode=TwoWay}"
SelectedItem="{Binding SelectedModelItem}" />
private tbl_Model _modelItem;
public tbl_Model SelectedModelItem
{
get { return _modelItem; }
private set
{
_modelItem = value;
NotifyPropertyChanged("SelectedModelItem");
}
}
public void Update()
{
SelectedModelItem.Model_No = "102";//Ui get notified, cause its a ModelItem from your Collection
}
ps: and pls remove the TwoWay Binding from
<ListView ItemsSource="{Binding ModelCollection,Mode=TwoWay}"
your ListeView will never set a ModelCollection back to your Viewmodel.
The properties of ModelItem also needs to implement INotifyPropertyChanged.
You have to implement INotifyPropertyChanged in your model.
In case your model is auto generated, then you can create a ViewModel for your model which implements INotifyPropertyChanged.
Each property of your Model or ViewModel needs to yield property changed.
The ObservableCollection raises events automatically but for ModelItem's properties you have to raise the events yourself.
public class ModelItem : INotifyPropertyChanged
{
private int modelNumber;
public int ModelNumber
{
get { return modelNumber; }
set
{
modelNumber = value;
NotifyPropertyChanged("ModelNumber"); }
}
//Similar implementation for other Properties Model Name, Manufacturer
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Trying to make my first application with the simple logging function to the TextBox on main form.
To implement logging, I need to get the TextBox object into the logger's class.
Prob - can't do that :) currently have no error, but as I understand the text value of TextBox is binding to my ViewModel, because getting 'null reference' exception trying to execute.
Logger.cs
public class Logger : TextWriter
{
TextBox textBox = ViewModel.LogBox;
public override void Write(char value)
{
base.Write(value);
textBox.Dispatcher.BeginInvoke(new Action(() =>
{
textBox.AppendText(value.ToString());
}));
}
public override Encoding Encoding
{
get { return System.Text.Encoding.UTF8; }
}
}
ViewModel.cs
public class ViewModel
{
public int ThreadCount { get; set; }
public int ProxyTimeout { get; set; }
public static TextBox LogBox { get; set; }
//private TextBox _LogBox;
//public TextBox LogBox {
// get { return _LogBox; }
// set {
// _LogBox = value;
// }
//}
}
launching on btn click, MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
Logger logger = new Logger();
logger.Write("ewgewgweg");
}
}
MainWindow.xaml
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:tools"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" x:Class="tools.MainWindow"
mc:Ignorable="d"
Title="Tools" Height="399.387" Width="575.46">
<TextBox x:Name="logBox"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" HorizontalAlignment="Left" Height="137" Margin="10,222,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="394" Text="{Binding Path = LogBox, Mode=TwoWay}"/>
You have several issues in your code:
Don't bring controls (TextBox) in your viewmodel, if you do there's no use in trying to do MVVM.
The Text property in XAML has to be of the type String or something that can be converted to a string. You're binding a control, which will result in showing System.Windows.Controls.TextBox (result of .ToString()) on your screen instead of actual text.
Your LogBox property should implement INotifyPropertyChanged
You don't want TwoWay binding, as the text flows from your logger to the UI, you don't need it to flow back. You might even consider using a TextBlock instead or make the control readonly so people can't change the content.
You don't want static properties or static viewmodels, read up on dependency injection on how to pass dependencies.
You will be flooding your UI thread by appending your characters one by one. Consider using another implementation (but I won't go deeper into this for this answer).
Keeping all above in mind, I transformed your code to this.
MainWindow.xaml
<TextBox x:Name="logBox"
HorizontalAlignment="Left" VerticalAlignment="Top" Height="137" Margin="10,222,0,0"
TextWrapping="Wrap" Width="394" Text="{Binding Path = LogBox}"/>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private Logger _logger;
public MainWindow()
{
InitializeComponent();
var viewModel = new ViewModel();
DataContext = viewModel;
_logger = new Logger(viewModel); // passing ViewModel through Dependency Injection
}
private void button1_Click(object sender, RoutedEventArgs e)
{
_logger.Write("ewgewgweg");
}
}
ViewModel.cs
public class ViewModel : INotifyPropertyChanged
{
public int ThreadCount { get; set; }
public int ProxyTimeout { get; set; }
private string _logBox;
public string LogBox
{
get { return _logBox; }
set
{
_logBox = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Logger.cs
public class Logger : TextWriter
{
private readonly ViewModel _viewModel;
public Logger(ViewModel viewModel)
{
_viewModel = viewModel;
}
public override void Write(char value)
{
base.Write(value);
_viewModel.LogBox += value;
}
public override Encoding Encoding
{
get { return System.Text.Encoding.UTF8; }
}
}
You can use string instead of TextBox as follow as
In view model class
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _logBox;
public string LogBox
{
get {return _logBox;}
set
{
if(value != _logBox)
{
_logBox=value;
OnPropertyChanged("LogBox");
}
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
and in writer method you just
public void writer (string str)
{
ViewModel.LogBox = str;
}
You can define ViewModel as static or create new object from ViewModel and access the object in logger class as you want!
hope this helped.
New to WPF and C# from VB web forms, so sorry for this poorly structured question I will add to as needed to improve. I am trying to implement an example by adding database calls to MySQL to populate an On-Demand Tree View control. Here is the link to the sample code...
sample code
Got my db connection working and data is populating my dataset. I iterate to place in a List. But can not seem to figure out the issue with passing the List to the Class to populate the control...
public class Level1
{
public Level1(string level1Name)
{
this.Level1Name = level1Name;
}
public string Level1Name { get; private set; }
readonly List<Level2> _level2s = new List<Level2>();
public List<Level2> Level2s
{
get { return _level2s; }
}
}
I have a database class that queries the db and parses the data....
List<string> level1s = new List<string>();
DataSet ds = new DataSet();
foreach (DataTable table in ds.Tables)
{
foreach (DataRow row in table.Rows)
{
level1s.Add((string)row["name"]);
}
}
**UPDATE**: Trying to return the list...
return new Level1[]
{
foreach(DataRow row in level1s)
{
// iterate here
}
};
My level1s List is properly populated, I am just drawing a blank on returning the values.
thanks,
UPDATE - I am including the ViewModel code here as well....
using BusinessLib;
namespace TreeViewWithViewModelTOC.LoadOnDemand
{
public class Level1ViewModel : TreeViewItemViewModel
{
readonly Level1 _level1;
public Level1ViewModel(Level1 level1)
: base(null, true)
{
_level1 = level1;
}
public string Level1Name
{
get { return _level1.Level1Name; }
}
protected override void LoadChildren()
{
foreach (Level2 level2 in Database.GetLevel2s(_level1))
base.Children.Add(new Level2ViewModel(level2, this));
}
}
}
Try like this below,
List<Level1> L1=new List<Level1>();
foreach(var row in level1s)
{
Level1 L=new Level1();
// L.Level1Name = row.ToString(); here add items as you need
L1.Add(L);
}
return L1.ToArray();
You should be using MVVM design pattern to solve this. There aren't many requirements listed in your questions so I will assume my own, which should lead you along the right path.
First thing is determining whether or not you're records are going to be ready/pulled at run-time--before the TreeView is rendered and if they will be changed/updated/added/removed from the structure during the lifecycle of the application. If the structure isn't going to be changed, you can continue to use List as your collection. If you're (or a user is) going to be adding/removing from the collection, ultimately changing the structure, then you need to notify the UI that a change occurred on the collection; so you would use the built in ObservableCollection for that. Here is a MVVM-purist solution, with the assumption that your data will be pulled at application startup and you will be modifying the collection:
Note: RelayCommand implementation was taken from here
Models
public class First
{
public string Name
{
get;
set;
}
public readonly List<Second> Children;
public First(string name)
{
Name = name;
Children = new List<Second>
{
new Second(1),
new Second(2),
new Second(3),
};
}
public void AddChild(Second child)
{
Children.Add(child);
ChildAdded(this, new ChildAddedEventArgs(child));
}
public EventHandler<ChildAddedEventArgs> ChildAdded;
}
public class ChildAddedEventArgs //technically, not considered a model
{
public readonly Second ChildAdded;
public ChildAddedEventArgs(Second childAdded)
{
ChildAdded = childAdded;
}
}
public class Second
{
public int Number
{
get;
set;
}
public Second(int number)
{
Number = number;
}
}
ViewModels
public class MainViewModel : INotifyPropertyChanged
{
private readonly ObservableCollection<FirstViewModel> _items;
private readonly ICommand _addFirstFirstChildCommand;
private readonly ICommand _addSecondFirstChildCommand;
private readonly ICommand _toggleExpandCollapseCommand;
private bool _firstAddedFlag;
public MainViewModel(IEnumerable<First> records)
{
_items = new ObservableCollection<FirstViewModel>();
foreach(var r in records)
{
_items.Add(new FirstViewModel(r));
}
_addFirstFirstChildCommand = new RelayCommand(param => AddFirst(), param => CanAddFirst);
_addSecondFirstChildCommand = new RelayCommand(param => AddSecond(), param => CanAddSecond);
_toggleExpandCollapseCommand = new RelayCommand(param => ExpandCollapseAll(), param =>
{
return true;
});
}
public ObservableCollection<FirstViewModel> Items
{
get
{
return _items;
}
}
public ICommand AddFirstFirstChildCommand
{
get
{
return _addFirstFirstChildCommand;
}
}
public ICommand AddSecondFirstChildCommand
{
get
{
return _addSecondFirstChildCommand;
}
}
public ICommand ToggleExpandCollapseCommand
{
get
{
return _toggleExpandCollapseCommand;
}
}
public bool CanAddFirst
{
get
{
return true;
}
}
public bool CanAddSecond
{
get
{
//Only allow second to be added if we added to first, first
return _firstAddedFlag;
}
}
public void AddFirstChild(FirstViewModel item)
{
Items.Add(item);
}
private void AddFirst()
{
_items[0].AddChild(new Second(10));
_firstAddedFlag = true;
}
private void AddSecond()
{
_items[1].AddChild(new Second(20));
}
private void ExpandCollapseAll()
{
foreach(var i in Items)
{
i.IsExpanded = !i.IsExpanded;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class FirstViewModel : INotifyPropertyChanged
{
private readonly First model;
private readonly ObservableCollection<SecondViewModel> _children;
private bool _isExpanded;
public FirstViewModel(First first)
{
_children = new ObservableCollection<SecondViewModel>();
model = first;
foreach(var s in first.Children)
{
Children.Add(new SecondViewModel(s));
}
model.ChildAdded += OnChildAdded;
}
public string FirstName
{
get
{
return model.Name;
}
set
{
model.Name = value;
NotifyPropertyChanged();
}
}
public ObservableCollection<SecondViewModel> Children
{
get
{
return _children;
}
}
public bool IsExpanded
{
get
{
return _isExpanded;
}
set
{
_isExpanded = value;
NotifyPropertyChanged();
}
}
internal void AddChild(Second second)
{
model.AddChild(second);
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void OnChildAdded(object sender, ChildAddedEventArgs args)
{
if(Children != null)
{
Children.Add(new SecondViewModel(args.ChildAdded));
}
}
}
public class SecondViewModel : INotifyPropertyChanged
{
private readonly Second model;
private bool _isExpanded;
public SecondViewModel(Second second)
{
model = second;
}
public int SecondNumber
{
get
{
return model.Number;
}
set
{
model.Number = value;
NotifyPropertyChanged();
}
}
//Added property to avoid warnings in output window
public bool IsExpanded
{
get
{
return _isExpanded;
}
set
{
_isExpanded = value;
NotifyPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Model Provider
public class Database
{
public static IEnumerable<First> GetChildren()
{
List<First> firsts = new List<First>();
firsts.Add(new First("John"));
firsts.Add(new First("Roxanne"));
return firsts;
}
}
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private MainViewModel mvm;
public MainWindow()
{
var db = Database.GetChildren();
mvm = new MainViewModel(db);
InitializeComponent();
DataContext = mvm;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
//Do not do this, example only
var f = new First("Billy");
mvm.AddFirstChild(new FirstViewModel(f));
//Prove that the event was raised in First, FirstViewModel see & handles it, and
//the UI is updated
f.AddChild(new Second(int.MaxValue));
}
}
MainWindow.xaml
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow">
<Grid>
<TreeView ItemsSource="{Binding Items}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:FirstViewModel}"
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding FirstName}" />
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:SecondViewModel}">
<TextBlock Text="{Binding SecondNumber}" />
</DataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded"
Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
<StackPanel Orientation="Vertical"
VerticalAlignment="Bottom">
<StackPanel Orientation="Horizontal">
<Button Content="Add Child to first First"
Command="{Binding AddFirstFirstChildCommand}" />
<Button Content="Toggle Expand"
Command="{Binding ToggleExpandCollapseCommand}" />
<Button Content="Add Child to second First"
Command="{Binding AddSecondFirstChildCommand}" />
</StackPanel>
<Button Content="Bad Codebehind Button"
Click="Button_Click"/>
</StackPanel>
</Grid>
</Window>
this returns array of Level1 from first table in DataSet (usually there's only one table)
public void Level1[] GetLevels()
{
DataSet ds = ....
return ds.Tables[0].Rows
.Select(row => new Level1((string)row["name"]))
.ToArray();
}
if you had more than one table in the dataset, you can use this method to loop trough all tables:
public void Level1[] GetLevels()
{
DataSet ds = ....
return ds.Tables
.SelectMany(t => t.Rows)
.Select(row => new Level1((string)row["name"]))
.ToArray();
}
The second code sample does exactly the same as your code in the question.
Understanding linq is extremely useful.
There must be a lot of questions surrounding this area but I couldn't find anything to help in my instance.
The problem I'm experiencing is getting my ViewModel, and specifically a property within ViewModel, to be updated to my View. Below is my implementation. I think I understand where I'm going wrong but not sure how to resolve it.
I have a Module that has a list and edit view. Quite simply lists domain objects and then ability to edit a domain object.
My xaml binds the DataContent to a ViewModel property in my View.
I then use the INavigationAware.NavigateTo method to navigate to my ViewModel and this is where I load the domain object.
The problem is that obviously this is not reflected back to the View. The view already has an instance of the ViewModel. This method worked fine when the ViewModel was using a list of objects using ObservableCollection. However, this did not work when using a simple object or even an ObservableObject.
Could someone please help my understanding or point me to some links with a better implementation of what I am trying to achieve?
MyModule
public class MyModule : IModule
{
private readonly IRegionManager _regionManager;
public MyModule(IRegionManager regionManager)
{
_regionManager = regionManager;
}
public void Initialize()
{
_regionManager.RegisterViewWithRegion(Constants.MainRegionName, typeof(MyListView));
_regionManager.RegisterViewWithRegion(Constants.MainRegionName, typeof(MyEditView));
}
}
XAML
<UserControl
DataContext="ViewModel">
...
<TextBlock Text="{Binding Path=MyDomainObject.AProperty}" />
...
View
public partial class MyEditView
{
public readonly static string ViewName = "MyEditView";
public MyEditView(MyEditViewModel viewModel)
{
InitializeComponent();
ViewModel = viewModel;
}
public MyEditViewModel ViewModel
{
get { return DataContext as MyEditViewModel; }
private set { DataContext = value; }
}
}
ViewModel
public class MyViewModel : INavigationAware
{
private readonly IRegionManager _regionManager;
public MyDomainObject MyDomainObject { get; set; }
public void Load(ViewModelKey key)
{
// get domain object
// this method worked when MyDomainObject was
// ObservableCollection<T> as just adding elements to list
// where this is creating a new instance of MyDomainObject
var id = parameter from navigationContext;
MyDomainObejct = server.GetDomainObject(id);
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
var key = key from navigationContext;
Load(key);
}
}
SOLUTION
public class MyEditViewModel : INavigationAware
{
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName]string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private MyDomainObject _myDomainObject;
public MyDomainObject MyDomainObject
{
get
{
return _myDomainObject;
}
set
{
if (value != _myDomainObject)
{
_myDomainObject = value;
NotifyPropertyChanged();
}
}
}
View
public partial class MyEditView
{
public MyEditView(MyEditViewModel viewModel)
{
InitializeComponent();
ViewModel = viewModel;
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
public MyEditViewModel ViewModel
{
get { return DataContext as MyEditViewModel; }
private set { DataContext = value; }
}
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (!(sender is MyEditViewModel))
return;
ViewModel = (MyEditViewModel)sender;
}
}
For your binding to update you need to implement INotifyPropertyChanged and raise PropertyChanged Event on the set accessor of your domain object.
public event PropertyChangedEventHandler PropertyChanged = delegate {};
public MyDomainObject MyDomainObject
{
get
{
return myDomainObject;
}
set
{
if(value != myDomainObject)
{
myDomainObject = value;
RaisePropertyChanged("MyDomainObject");
}
}
}
private void RaisePropertyChanged(String p)
{
PropertyChanged(this, new PropertyChangedEventArgs(p));
}
Or as in the Prism book, inherit NotificationObject and call RaisePropertyChanged(()=> PropertyName) which is refactoring-safe