How to handle changes of submodels in main model (MVVM) - c#

What is the best practice to update dynamically created checkboxes states from model? Acutal values for checkboxes are held in submodels of main model and being changed accordingly to it's logic. Checkboxes' properties bind to their individual FooViewModels. But how to change FooViewModel's properties then?
1 way: main model fires especial event -> main VM handles it and finds target FooViewModel to update using event args -> main VM sets target FooViewModel properties with values specified in event args -> checkbox is updated via bindings to FooViewModel
2 way: Main model holds observable collection of FooModels implementing INPC and each is being wrapped with FooViewModel (using CollectionChanged event in main VM). Main model set some FooModel's property -> FooViewModel handles PropertyChanged and transfers it further firing own PropertyChanged event -> checkbox is updated via bindings to FooViewModel.
Transferrence code in FooViewModel:
this._model.PropertyChanged += (s, a) => this.RaisePropertyChangedEvent(a.PropertyName);
My implementation of 2nd way is next:
// MainModel class that holds collection of extra models (CfgActionModel):
class MainModel: BindableBase
{
ObservableCollection<CfgActionModel> _actionsColl
= new ObservableCollection<CfgActionModel>();
public ObservableCollection<CfgActionModel> ActionCollection
{
get => this._actionsColl;
}
public void AddAction(ConfigEntry cfgEntry, bool isMeta)
{
CfgActionModel actionModel = new CfgActionModel()
{
CfgEntry = cfgEntry,
Content = cfgEntry.ToString(),
IsEnabled = true,
IsChecked = false
};
this._actionsColl.Add(actionModel);
}
}
// Extra model that is wrapped with CfgActionViewModel:
class CfgActionModel: BindableBase
{
ConfigEntry _cfgEntry; // Custom enumeration value unique for each checkbox
string _content;
bool _isEnabled = false;
bool _isChecked = false;
public ConfigEntry CfgEntry
{
get => this._cfgEntry;
set
{
if (this._cfgEntry == value) return;
this._cfgEntry = value;
this.RaisePropertyChangedEvent(nameof(CfgEntry));
}
}
public string Content
{
get => this._content;
set
{
if (this._content == value) return;
this._content = value;
this.RaisePropertyChangedEvent(nameof(Content));
}
}
public bool IsEnabled
{
get => this._isEnabled;
set
{
if (this._isEnabled == value) return;
this._isEnabled = value;
this.RaisePropertyChangedEvent(nameof(IsEnabled));
}
}
public bool IsChecked
{
get => this._isChecked;
set
{
if (this._isChecked == value) return;
this._isChecked = value;
this.RaisePropertyChangedEvent(nameof(IsChecked));
}
}
}
// CfgActionViewModel that is checkbox in UI is bound to:
class CfgActionViewModel: BindableBase
{
CfgActionModel _model;
public CfgActionViewModel(CfgActionModel model)
{
this._model = model;
this._model.PropertyChanged += (s, a) => this.RaisePropertyChangedEvent(a.PropertyName);
}
public string Content
{
get => this._model.Content;
set => this._model.Content = value;
}
public bool IsEnabled
{
get => this._model.IsEnabled;
set => this._model.IsEnabled = value;
}
public bool IsChecked
{
get => this._model.IsChecked;
set => this._model.IsChecked = value;
}
}
// MainViewModel where we fill the model with data:
class MainViewModel
{
MainModel model;
readonly ObservableCollection<CfgActionViewModel> _actionVMColl = new ObservableCollection<CfgActionViewModel>();
public ObservableCollection<CfgActionViewModel> ActionVMCollection => this._actionVMColl;
public MainViewModel()
{
this.model = new MainModel();
this.model.ActionCollection.CollectionChanged += (s, a) =>
{
// when new model is created we create new ViewModel wrapping it
if (a.Action == NotifyCollectionChangedAction.Add)
{
CfgActionModel newModel = (CfgActionModel) a.NewItems[0];
CfgActionViewModel actionViewModel = new CfgActionViewModel(newModel);
_actionVMColl.Add(actionViewModel);
}
};
model.AddAction(ConfigEntry.AutoBuy, false);
model.AddAction(ConfigEntry.Bomb, false);
}
}
DataTemplate in View looks like this:
<DataTemplate DataType="{x:Type mvvm:CfgActionViewModel}">
<CheckBox
IsChecked="{Binding Path=IsChecked, Mode=TwoWay}"
IsEnabled="{Binding Path=IsEnabled, Mode=TwoWay}"
Content="{Binding Path=Content, Mode=OneWay}"/>
</DataTemplate>
Is it acceptable by MVVM to avoid interaction with MainViewModel somewhere (2nd way) or each subViewModel's property must be set by MainViewModel (1st way)?

Both approaches are acceptable. But personally, I would do approach #1 to keep my Models as thin as possible.
You can refer to the sample code on how you can do approach #1.
public class MainViewModel : BindableBase
{
public ObservableCollection<SubViewModel> SubViewModels { get; }
public MainViewModel()
{
SubViewModels = new ObservableCollection<SubViewModel>();
SubViewModels.CollectionChanged += SubViewModels_CollectionChanged;
}
private void SubViewModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e.Action == NotifyCollectionChangedAction.Add)
{
foreach(var subVM in e.NewItems.Cast<SubViewModel>())
{
subVM.PropertyChanged += SubViewModel_PropertyChanged;
}
}
// TODO: Unsubscribe to SubViewModels that are removed in collection to avoid memory leak.
}
private void SubViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(SubViewModel.IsChecked):
// TODO: Do your thing here...
break;
}
}
}
public class SubViewModel : BindableBase
{
private bool _isChecked;
public bool IsChecked
{
get => _isChecked;
set => SetProperty(ref _isChecked, value);
}
}
As you can see, I don't even need to include any Models in the sample code which means that all the logic here are all clearly part of the presentation layer.
Now, you can focus on your business/domain logic in your Models.

Related

AvaloniaUI StyledProperty and MVVM binding doesn't update

I'm using AvaloniaUI and making a UserControl with a StyledProperty using. I'm using MVVM.
The problem is that when I have a Binding to my styledproperty but it doesn't update.
I want to use the UserControl like this <Comp:ValueTextBlock VariableName="{Binding RobotSettingsModel.Robot_SP}"/> where VariableName uses a binding to a model that is created in the ViewModel.
The problem is that I can't seem to get the StyledProperty to work when I use a binding. When I set VariableName directly in the view it does work
// this works
<Comp:ValueTextBlock VariableName="PLC_U1_Robot_SP"/>
// this doesn't
<Comp:ValueTextBlock VariableName="{Binding RobotSettingsModel.Robot_SP}" DescriptionLocation="Left"/>
What am I doing wrong here?
The code-behind for my UserControl ValueTextBlock looks something like this:
public class ValueTextBlock : UserControl
{
private ValueTextBlockVm _viewModel;
#region --- Variable name properties ---
public static readonly StyledProperty<string> VariableNameProperty = AvaloniaProperty.Register<ValueTextBlock, string>(nameof(VariableName), defaultBindingMode: BindingMode.TwoWay, defaultValue: "UNKNOWN DProperty");
public string VariableName
{
get { return _viewModel.vmVariableName; } // the property is used in the ViewModel
set { _viewModel.vmVariableName = value; }
}
#endregion
#region constructor
public ValueTextBlock()
{
this.InitializeComponent();
// create new instance of viewmodel and attach it as DataContext
_viewModel = new ValueTextBlockVm();
this.DataContext = _viewModel;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
#endregion
}
In the ViewModel for ValueTextBlock vmVariableName is done like this:
private string _vmVariableName = "UKN";
public string vmVariableName
{
get => _vmVariableName;
set => this.RaiseAndSetIfChanged(ref _vmVariableName, value);
}
When i use this UserControl and directly set VariableName in view it works, but when I use Binding it doesn't work.
This is my view:
<StackPanel>
<Comp:ValueTextBlock VariableName="PLC_U1_Robot_SP"/> <!-- directly setting VariableName works -->
<Comp:ValueTextBlock VariableName="{Binding RobotSettingsModel.Robot_SP}" DescriptionLocation="Left"/> <!-- binding VariableName to a model doesn't work -->
<TextBlock Text="{Binding RobotSettingsModel.Robot_SP}"/> <!-- binding normal text to a model does work-->
</StackPanel>
Code-behind for the view
public class ucRobotSettings : UserControl
{
private ucRobotSettingsVm _viewModel;
#region properties
public string Prefix
{
get { return _viewModel.vmPrefix; }
set { _viewModel.vmPrefix = value; }
}
public static readonly StyledProperty<string> PrefixProperty = AvaloniaProperty.Register<ucRobotSettings, string>(nameof(Prefix));
#endregion
#region constructor
public ucRobotSettings()
{
this.InitializeComponent();
// create new instance of viewmodel and attach it as DataContext
_viewModel = new ucRobotSettingsVm();
this.DataContext = _viewModel;
this.AttachedToVisualTree += ucRobotSettings_AttachedToVisualTree;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
#endregion
private void ucRobotSettings_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
_viewModel.OnAttachedToVisualTree();
}
}
In the ViewModel a new RobotSettingsModel is made, this is what I want to bind to in the View
public class ucRobotSettingsVm : ViewModelBase
{
#region --- properties ---
private string _vmPrefix;
public string vmPrefix
{
get => _vmPrefix;
set => this.RaiseAndSetIfChanged(ref _vmPrefix, value);
}
private RobotSettingsModel _RobotSettingsModel = new RobotSettingsModel();
public RobotSettingsModel RobotSettingsModel
{
get => _RobotSettingsModel;
set => this.RaiseAndSetIfChanged(ref _RobotSettingsModel, value);
}
#endregion
public ucRobotSettingsVm() { }
public void OnAttachedToVisualTree()
{
// don't update if the prefix hasn't changed
if (RobotSettingsModel.Prefix != vmPrefix) RobotSettingsModel.Prefix = vmPrefix;
}
}
The model that is used in ucRobotSettings looks like this:
public class RobotSettingsModel : ReactiveObject
{
// unit prefix.
private string _Prefix;
public string Prefix { get => _Prefix; set { this.RaiseAndSetIfChanged(ref _Prefix, value); NotifyPropertyChanged(); } }
// values
private string _Robot_SP = "UKN";
public string Robot_SP { get => _Robot_SP; set => this.RaiseAndSetIfChanged(ref _Robot_SP, value); }
public RobotSettingsModel()
{ }
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String propertyName = "")
{
if (propertyName == "Prefix") // only update when property "Prefix changes"
{
Robot_SP = Prefix + "_" + nameof(Robot_SP);
// inform outside outside world the complete class has PropertyChanged
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

C# Wpf Combobox set selectedvalue, not change selecteditem in UI

i have a combobox inside a usercontrol
<ComboBox DisplayMemberPath="CustomerCollection.Description1" SelectedValuePath="CustomerCollection.CustomerID"
SelectedValue="{Binding Order.OrderCollection.CustomerID, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding Customers, UpdateSourceTrigger=PropertyChanged}">
</ComboBox>
My view model have a class model Order, that have a property CustomerID, and inside the view model, i have an observableCollection of customers
private OrderModel _Order;
public OrderModel Order
{
get
{
return _Order;
}
set
{
SetProperty(ref _Order, value);
}
}
private ObservableCollection<CustomerModel> _Customers;
public ObservableCollection<CustomerModel> Customers
{
get { return _Customers; }
set { SetProperty(ref _Customers, value); }
}
Everything works, but when i set the Order.OrderCollection.CustomerID, so the SelectedValue of my combobox, in the ui not update the selecteditem
private void ReloadCustomers(object CustomerID)
{
DialogVisibility = System.Windows.Visibility.Visible;
BackgroundWorker bgwLoad = new BackgroundWorker();
bgwLoad.DoWork += delegate (object s, DoWorkEventArgs args)
{
Customers = new ObservableCollection<CustomerModel>(_customerRepository.Get());
};
bgwLoad.RunWorkerCompleted += (sender, eventArgs) =>
{
Order.OrderCollection.CustomerID = (long)CustomerID;
_eventAggregator.GetEvent<UIX_MessageEventAggregator.PassParameter>().Unsubscribe(ReloadCustomers);
};
bgwLoad.RunWorkerAsync();
}
i try to implement the override of equals inside Customer, but the stil not working, the only way to get work, seems to create a property of customer inside the viewmodel and binding to selecteditem, but i don't like
public override bool Equals(object obj)
{
var customer = obj as CustomerModel;
if (customer != null)
return customer.CustomerCollection.CustomerID == CustomerCollection.CustomerID;
else
return false;
}
public override int GetHashCode()
{
return CustomerCollection.CustomerID.GetHashCode();
}
i know i missing something, but i can't understand what?
Someone have idea?
public class OrderCollection : _CompanyModel
{
public OrderCollection(string Vat) : base(Vat)
{
}
#region Core
private string _OrderID;
public string OrderID
{
get { return _OrderID; }
set { SetProperty(ref _OrderID, value); }
}
private long _CustomerID;
public long CustomerID
{
get { return _CustomerID; }
set { SetProperty(ref _CustomerID, value); }
}
#endregion
}
Ok the problem is the BackgroundWorker
This work:
private void ReloadCustomers(object CustomerID)
{
DialogVisibility = System.Windows.Visibility.Visible;
Customers = new ObservableCollection<CustomerModel>(_customerRepository.Get());
Order.OrderCollection.CustomerID = (long)CustomerID;
}
This not:
private void ReloadCustomers(object CustomerID)
{
DialogVisibility = System.Windows.Visibility.Visible;
BackgroundWorker bgwLoad = new BackgroundWorker();
bgwLoad.DoWork += delegate (object s, DoWorkEventArgs args)
{
Customers = new ObservableCollection<CustomerModel>(_customerRepository.Get());
};
bgwLoad.RunWorkerCompleted += (sender, eventArgs) =>
{
Order.OrderCollection.CustomerID = (long)CustomerID;
_eventAggregator.GetEvent<UIX_MessageEventAggregator.PassParameter>().Unsubscribe(ReloadCustomers);
};
bgwLoad.RunWorkerAsync();
}
as you did not post the content of the OrderCollection class this is just an assumption. But my best guess is that you don't raise a PropertyChangedEvent when you set the CustomerID property in the OrderCollection.
Therefore the View never gets notified about the change in selection.
So changing the model implementation to raise a property changed even for your CustomID should fix the issue.
However you might not want to pollute your Model with such events. Another option could be to wrap your models (Customer, Order) in dedicated ViewModels and raise the events in there where needed.
The issue was inside BackgroundWorker, i've repleced with
await Task.Run(() =>
{
Customers = new ObservableCollection<CustomerModel>(_customerRepository.Get());
})
.ContinueWith(t =>
{
Order.OrderCollection.CustomerID = (long)CustomerID;
_eventAggregator.GetEvent<UIX_GlobalEvent.PassParameter>().Unsubscribe(ReloadCustomers);
});

How to check a duplicate entry in a list of models?

I have a MVVM app with a ListView composed of EditableTextblocks in a DataTemplate (like this).
Here is my model :
public class MyModel
{
private string _data;
public string Data
{
get { return _data; }
set { _data = value; }
}
}
My viewmodel exposes an ObservableCollection of MyModel:
public class MyViewModel
{
[...]
public ObservableCollection<Mymodel> models = new ObservableCollection<MyModel>();
}
and in the view bound to a ListView:
<ListView ItemsSource={Binding models}>
<!-- code removed for more visibility -->
<DataTemplate>
<controls:EditableTextblock Text="{Binding Data, Mode=TwoWay}" />
</DataTemplate>
<!-- ... -->
</ListView>
Would you have any leads that when in an item in the list I update the value of a Data member, there is a check to see if a value already exists in the collection?
For example, if I update a field to "value 1", it checks if in the models collection there is a member Data that already has this value.
And if it found one, it adds for example a "0" at the end of the member Data.
Provided that the MyModel class implements the INotifyPropertyChanged and raises the PropertyChanged event when the Data property is set, you could handle this in the view model:
public class MyViewModel
{
public ObservableCollection<MyModel> models = new ObservableCollection<MyModel>();
public MyViewModel()
{
models.CollectionChanged += Models_CollectionChanged;
}
private void Models_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (object model in e.NewItems)
{
(model as INotifyPropertyChanged).PropertyChanged
+= new PropertyChangedEventHandler(Model_PropertyChanged);
}
}
if (e.OldItems != null)
{
foreach (object model in e.OldItems)
{
(model as INotifyPropertyChanged).PropertyChanged
-= new PropertyChangedEventHandler(Model_PropertyChanged);
}
}
}
private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
MyModel updatedModel = sender as MyModel;
MyModel duplicate = models.FirstOrDefault(x => x != updatedModel && x.Data == updatedModel.Data);
if(duplicate != null)
{
updatedModel.Data += "0";
}
}
}
public class MyModel : INotifyPropertyChanged
{
private string _data;
public string Data
{
get { return _data; }
set { _data = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
For something like this, I typically wrap my models in their own view-model class, and add the validation there. The wrapper takes the instance of the original model to wrap, plus a reference to the parent view-model, so that it can check for duplicates or do other operations with the parent.
The validation could also be done without a reference to the parent if you use some kind of messaging system, like MVVMLight's Messenger class to communicate between view-models.
The main reason I wrap them this way, is because I like to keep the models "pure", without any change notification, WPF, or business logic in them, beyond what is directly required for their domain. This allows me to keep the models as simple data classes, and move any business or view-specific logic to someplace more appropriate.
Your existing classes (note, I changed the collection to be the wrapper class):
public class MyViewModel : BaseViewModel //whatever base class you use to notify of property changes.
{
[...]
public ObservableCollection<MyModelVm> models = new ObservableCollection<MyModelVm>();
}
public class MyModel
{
private string _data;
public string Data
{
get { return _data; }
set { _data = value; }
}
}
The new wrapper view-model:
public class MyModelVm : BaseViewModel //whatever base class you use to notify of property changes.
{
public MyModelVm(MyModel model, MyViewModel parentViewModel)
{
Model = model;
ParentViewModel = parentViewModel;
}
public MyModel Model { get; }
public MyViewModel ParentViewModel { get; }
public string Data
{
get { return Model.Data; }
set
{
if (ParentViewModel.models.Any(x => x != this && x.Data == this.Data))
{
//Duplicate entered
}
else
{
//Not a duplicate, go ahead and allow the change.
Model.Data = value;
//don't forget to notify of property change!
}
}
}
}

How to bind to child view model property in WPF application?

I am developing WPF application with Prism MVVM framework. And I doesn't know how properly pass data between parent and child view models.
I have 2 view models - ParentViewModel and inner ChildViewModel.
public class ParentViewModel
{
public ParentViewModel
{
ChildViewModel = new ChildViewModel(params);
}
private ChildViewModel _childViewModel;
public ChildViewModel ChildViewModel
{
get { return _childViewModel; }
set
{
SetProperty(ref _childViewModel, value);
}
}
//This is does not work
public int SelectedChildNumber
{
return _childViewModel.SelectedNumber;
}
}
public class ChildViewModel
{
public ChildViewModel
{
_numbers = new List<int>();
}
private List<int> _numbers;
public List<int> Numbers
{
get { return _numbers; }
set
{
SetProperty(ref _numbers, value);
}
}
private int _selectedNumber;
public int SelectedNumber
{
get { return _selectedNumber; }
set
{
SetProperty(ref _selectedNumber, value);
}
}
}
I want to get and use selected value from child view model. My approach doesn't work - SelectedChildNumber doesn't want to refresh if SelectedNumber changes in ChildViewModel.
UPDATE:
Ok, What if I have ChildViewModel collection in ParentViewModel. One of this ChildViewModels have property IsSelected equals true. How to get this one selected view model from collection?
public class ParentViewModel
{
public ParentViewModel
{
Items = GetItems();
}
private ObservableCollection<ChildViewModel> _items;
public ObservableCollection<ChildViewModel> Items
{
get
{
return _items;
}
set
{
SetProperty(ref _items, value);
}
}
}
public class ChildViewModel
{
public ChildViewModel
{
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
SetProperty(ref _isSelected, value);
}
}
}
How to get selected view model? Maybe use a converter?
<someUserControl DataContext="{Binding ParentViewModel.Items, Converter={x:Static c:GetSelectedItemConverter.Instance}}" />
In converter I can find selected item. Or this is bad idea?
UPDATE 2:
Ok, I beat this problem with Ed Plunkett help. Final version should be:
public class ParentViewModel
{
public ParentViewModel
{
Items = GetItems();
foreach (var item in Items)
{
item.PropertyChanged += ChildViewModel_PropertyChanged;
}
}
private ObservableCollection<ChildViewModel> _items;
public ObservableCollection<ChildViewModel> Items
{
get
{
return _items;
}
set
{
SetProperty(ref _items, value);
}
}
private ChildViewModel _selectedChild;
public ChildViewModel SelectedChild
{
get { return _selectedChild; }
set
{
SetProperty(ref _selectedChild, value);
}
}
private void ChildViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var child = (ChildViewModel)sender;
if (e.PropertyName == nameof(ChildViewModel.IsSelected) && child.IsSelected)
{
SelectedChild = child;
}
}
}
Bind directly to the child property:
<ListBox
ItemsSource="{Binding ChildViewModel.Numbers}"
SelectedItem="{Binding ChildViewModel.SelectedNumber}"
/>
<Label Content="{Binding ChildViewModel.SelectedNumber}" />
That's the name of the parent's ChildViewModel property in the binding path, not the type. The Binding now knows to listen to the ChildViewModel object for PropertyChanged notifications regarding SelectedNumber and Numbers.
The reason your version doesn't work is that the parent does not raise PropertyChanged when SelectedChildNumber changes. In fact, the parent doesn't know when it changes any more than the UI does. The parent could handle the child's PropertyChanged event, and sometimes that's done.
public ParentViewModel()
{
ChildViewModel = new ChildViewModel(params);
// Handle child's PropertyChanged event
ChildViewModel.PropertyChanged += ChildViewModel_PropertyChanged;
}
private void ChildViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var child = (ChildViewModel)sender;
if (e.PropertyName == nameof(ChildViewModel.SelectedNumber))
{
// Do stuff
}
}
But you don't need to do that for cases like this one.
ChildViewModel.Numbers should probably be ObservableCollection<int>, not List<int>. That way, if you add more numbers to it or remove any, the UI will automatically be notified by the collection and the ListBox will automatically update itself accordingly.

wpf - One ViewModel for dynamic Tabs

Is it possible to have one ViewModel for multiple dynamic Tabs? Meaning that, whenever I create a new tab, it should use the same instance of ViewModel so I can retrieve information and also prevent each Tab from sharing data/showing the same data.
The setting I'm thinking of using it in would be for a payroll application where each employee's payslip can be updated from each tab. So the information should be different in each Tab.
Is this possible?
Update: Added code
MainViewModel where Tabs Collection is handled:
public ObservableCollection<WorkspaceViewModel> Workspaces { get; set; }
public MainViewModel()
{
Workspaces = new ObservableCollection<WorkspaceViewModel>();
Workspaces.CollectionChanged += Workspaces_CollectionChanged;
}
void Workspaces_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null && e.NewItems.Count != 0)
foreach (WorkspaceViewModel workspace in e.NewItems)
workspace.RequestClose += this.OnWorkspaceRequestClose;
if (e.OldItems != null && e.OldItems.Count != 0)
foreach (WorkspaceViewModel workspace in e.OldItems)
workspace.RequestClose -= this.OnWorkspaceRequestClose;
}
private void OnWorkspaceRequestClose(object sender, EventArgs e)
{
CloseWorkspace();
}
private DelegateCommand _exitCommand;
public ICommand ExitCommand
{
get { return _exitCommand ?? (_exitCommand = new DelegateCommand(() => Application.Current.Shutdown())); }
}
private DelegateCommand _newWorkspaceCommand;
public ICommand NewWorkspaceCommand
{
get { return _newWorkspaceCommand ?? (_newWorkspaceCommand = new DelegateCommand(NewWorkspace)); }
}
private void NewWorkspace()
{
var workspace = new WorkspaceViewModel();
Workspaces.Add(workspace);
SelectedIndex = Workspaces.IndexOf(workspace);
}
private DelegateCommand _closeWorkspaceCommand;
public ICommand CloseWorkspaceCommand
{
get { return _closeWorkspaceCommand ?? (_closeWorkspaceCommand = new DelegateCommand(CloseWorkspace, () => Workspaces.Count > 0)); }
}
private void CloseWorkspace()
{
Workspaces.RemoveAt(SelectedIndex);
SelectedIndex = 0;
}
private int _selectedIndex = 0;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
_selectedIndex = value;
OnPropertyChanged("SelectedIndex");
}
}
WorkspaceViewModel:
public PayslipModel Payslip { get; set; }
public WorkspaceViewModel()
{
Payslip = new PayslipModel();
SaveToDatabase = new DelegateCommand(Save, () => CanSave);
SelectAll = new DelegateCommand(Select, () => CanSelect);
UnSelectAll = new DelegateCommand(UnSelect, () => CanUnSelect);
}
public ICommand SaveToDatabase
{
get; set;
}
private bool CanSave
{
get { return true; }
}
private async void Save()
{
try
{
MessageBox.Show(Payslip.Amount.ToString());
}
catch (DbEntityValidationException ex)
{
foreach (var en in ex.EntityValidationErrors)
{
var exceptionDialog = new MessageDialog
{
Message = { Text = string.Format("{0}, {1}", en.Entry.Entity.GetType().Name, en.Entry.State) }
};
await DialogHost.Show(exceptionDialog, "RootDialog");
foreach (var ve in en.ValidationErrors)
{
exceptionDialog = new MessageDialog
{
Message = { Text = string.Format("{0}, {1}", ve.PropertyName, ve.ErrorMessage) }
};
await DialogHost.Show(exceptionDialog, "RootDialog");
}
}
}
catch (Exception ex)
{
var exceptionDialog = new MessageDialog
{
Message = { Text = string.Format("{0}", ex) }
};
await DialogHost.Show(exceptionDialog, "RootDialog");
}
}
public event EventHandler RequestClose;
private void OnRequestClose()
{
if (RequestClose != null)
RequestClose(this, EventArgs.Empty);
}
private string _header;
public string Header
{
get { return _header; }
set
{
_header = value;
OnPropertyChanged("Header");
}
}
Payroll UserControl where WorkspaceViewModel is DataContext:
public Payroll()
{
InitializeComponent();
DataContext = new WorkspaceViewModel();
}
Payroll.xaml Tabcontrol:
<dragablz:TabablzControl ItemsSource="{Binding Workspaces}" SelectedIndex="{Binding SelectedIndex}" BorderBrush="{x:Null}">
<dragablz:TabablzControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
</dragablz:TabablzControl.ItemTemplate>
<dragablz:TabablzControl.ContentTemplate>
<DataTemplate>
<ContentControl Margin="16">
<local:TabLayout DataContext="{Binding Path=Payslip, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" x:Name="tabLayout"/>
</ContentControl>
</DataTemplate>
</dragablz:TabablzControl.ContentTemplate>
</dragablz:TabablzControl>
This works as expected, each tab displays different info and bindings work okay. However, I'm unable to retrieve the info in the MessageBox.
I'm not sure if I totally understand your question but if you need a Window with a tabcontrol, in which each tab refers to an employee, then you will have to bind the ItemsSource of the tabcontrol to a list of the ViewModel.
It is not possible to bind all tabpages to the same instance because then the tabpages will all do the same, and show the same information.
I couldn't get it to work the way I had it, so I placed the save button inside the view that has DataContext set to where employee's info are loaded and got it to work from there, since it directly accesses the properties.
ViewModels should have a 1:1 relationship with the model. In your TabControl's DataContext, let's say you have properties like:
public ObservableCollection<EmployeeViewModel> Employees {get;set;}
public EmployeeViewModel CurrentEmployee
{
get { return _currentEmployee;}
set
{
_currentEmployee = value;
OnPropertyChanged("CurrentEmployee");
}
}
where Employees is bound to ItemsSource of the TabControl, and CurrentEmployee to CurrentItem. To create a new tab:
var employee = new Employee();
var vm = new EmployeeViewModel(employee);
Employees.Add(vm);
CurrentEmployee = vm;
If you want a save button outside of the TabControl, just set its DataContext to CurrentEmployee.
I hope this helps!
Edit:
Two things I think are causing problems:
Payroll.xaml should be bound to MainViewModel since that's where the Workspaces collection is.
Do not instantiate ViewModels in your view's code behind. Use a DataTemplate instead (see this question).
Take a look at Josh Smith's MVVM demo app (source code)

Categories