When trying to bind a ListView to an ObservableCollection via XAML, the ListView is not updated and is initially loaded with blank values.
Via XAML
History.xaml.cs
DataContext = this;
History.xaml:
<ListView x:Name="lvHistory" ItemsSource="{Binding Source=history}" BorderThickness="0" Margin="0,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Column="2" util:GridViewSort.AutoSort="True" SizeChanged="lvHistory_SizeChanged">
Via CODE
When doing the binding via code, the bindings work correctly.
History.xaml
<ListView x:Name="lvHistory" BorderThickness="0" Margin="0,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Column="2" util:GridViewSort.AutoSort="True" SizeChanged="lvHistory_SizeChanged">
History.xaml.cs
DataContext = this;
lvHistory.ItemsSource = history;
By simply adding the ItemsSource via code and removing it in XAML, the code works properly. What am I missing? How do I create the bindings via pure XAML?
history:
public ObservableCollection<LocateElement> history { get; private set; }
Code for updating the list:
public void Update()
{
if (updater.IsBusy) updatePending = true;
else
{
searchValue = txtSearch.Text.Trim();
updatePending = false;
updater.RunWorkerAsync();
}
}
private void updateContent(object sender, DoWorkEventArgs e)
{
try
{
Globals.Variables.logger.Info("Locate History: Updating");
using (var db = new Data.DataManager())
{
var history = db.LocateHistory.Where(o => o.ReceivedBy == Globals.Variables.loginDetails.UserID);
e.Result = filterResults(history);
}
}
catch (Exception er)
{
Globals.Variables.logger.Error(er);
}
}
private void updateFinished(object sender, RunWorkerCompletedEventArgs e)
{
List<LocateElement> r = (List<LocateElement>)e.Result;
history.Clear();
foreach (LocateElement l in r)
{
history.Add(l);
}
if (updatePending) Update();
//else Wpf.Util.GridViewSort.ReapplySort(lvHistory);
}
private List<LocateElement> filterResults(IQueryable<LocateElement> list)
{
List<LocateElement> history = new List<LocateElement>();
foreach (LocateElement l in list)
{
if (searchValue != "")
{
// Use the parameters to filter the results.
Regex reg = new Regex(WildcardToRegex(searchValue));
if (reg.IsMatch(l.Serial) || reg.IsMatch(l.Asset) || reg.IsMatch(l.DeviceType) || reg.IsMatch(l.Company) || (l.ReceivedFrom != null && reg.IsMatch(l.ReceivedFrom.Name)) || (l.ReceivedTo != null && reg.IsMatch(l.ReceivedTo.Name)) || reg.IsMatch(l.Row) || reg.IsMatch(l.Shelf) || reg.IsMatch(l.Bin) || reg.IsMatch(l.DateReceived.ToString()))
{
history.Add(l);
}
}
else
{
history.Add(l);
}
}
return history;
}
When you assign data to your history collection you need to make sure you raise the property changed event.
For example:
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<LocateElement> _history;
public ObservableCollection<LocateElement> history
{
get { return _history; }
set
{
if (_history != value)
{
_history = value;
RaisePropertyChanged("history");
}
}
}
public MyViewModel()
{
_history = new ObservableCollection<LocateElement>();
}
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The Source property of a Binding doesn't mean what you think it means. Use Path instead or let it assume you're talking about Path (default). This should do it.
<ListView ItemsSource="{Binding history}" ...>
Additionally, if you're setting the history property outside your constructor it needs to notify of property changed. IF you're only setting it in your constructor you won't need to but you might want to make it backed by a readonly field instead of an auto getter/setter. (TrueEddie's solution describes this problem and supplies the solution for being able to swap out the actual variable).
Related
I will try to keep this as brief as possible, but there is a fair amount of nuance to this question.
The Workflow
I am working in C# and am using WPF and MVVM for the UI for an addin for Revit (a 3D modeling software from Autodesk).
The overarching goal is to create a window that shows the parameters of a 3D element after it is selected. This is a filtered list that are specific to my organization and our users needs, and allow the user to edit them in order to streamline their workflow. The complication is that because I am working with an API I can only use what tools I am given when interacting with the model.
The issue I am running into lies in the workflow. I have detailed the workflow below.
User Selects a 3D Element
The addin uses the API to pull the parameters and wraps them in a wrapper class and adds them into a custom ObservableCollection to display them in the DataGrid
The user then changes a value in the DataGrid. When the cell loses focus it fires off a command that hooks the API and updates the parameter's value.
The change is made and the internal logic of the element calculates it's values based on the changed parameter
The calculated parameter values are changed in the model
The ViewModel checks each parameter to see if its value has changed, and updates any of the wrapped parameters in the ObservableCollection to reflect the changes.
The ObservableCollection fires off it's collection changed event to notify the DataGrid that values have changed
The DataGrid updates it's values to complete the process.
The issue currently lies in the very last step. Once the collection changed event is complete the wrapped parameter value matches the parameter value from the API, but the DataGrid will not redraw the information. Once you minimize the window, click into the cell, or scroll the DataGrid to where the cell is not visible the cell will show the new value when it comes back into view.
I can't seem to find a way to keep with MVVM principles and force the cells to redraw with their updated value. Am I missing something with this? How do I get the DataGrid to update without having to completely clear and reset the ObservableCollection items?
Things I have Tried
I had to create a custom ObservableCollection to implement INPC for the items in the collection, and from debugging it appears to work as intended. Each time an item in the ObservableCollection is updated it makes the change subscribes it to INPC and raises the collection changed event.
For each of the columns I have the binding set to Mode="TwoWay" and have tried setting the UpdateSourceTrigger="PropertyChanged", and neither helped.
I originally was using a <ContentPresenter/> in a <DataGridTemplteColumn/> to present different cell types, but even using a basic <DataGridTextColumn doesn't work.
---- CODE ----
XAML:
<DataGrid Grid.Row="2" ItemsSource="{Binding TestingParameters, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn IsReadOnly="True" Binding="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Name"/>
<DataGridTextColumn IsReadOnly="False" Binding="{Binding Path=ParamValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Header="Param Value">
<i:Interaction.Triggers>
<i:EventTrigger EventName="LostFocus">
<i:InvokeCommandAction Command="{Binding UpdateParametersCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
C# ViewModel
public class ViewModelParameterPane : ViewModelBase
{
private ExternalEvent _event;
private HandlerED _handler;
private UIApplication _uiapp;
private UIDocument _uidoc;
private Document _doc;
private TestObservableCollection<WrappedParameter> _testParameter = new TestObservableCollection<WrappedParameter>();
public TestObservableCollection<WrappedParameter> TestParameter
{
get => _testParameter;
set
{
_testParameter = value;
RaiseProperty(nameof(_testParameter));
}
}
public ViewModelParameterPane(ExternalEvent exEvent, HandlerED handler, UIApplication uiapp)
{
_event = exEvent;
_handler = handler;
_uiapp = uiapp;
_uidoc = _uiapp.ActiveUIDocument;
_doc = _uidoc.Document;
_testParameter.ItemPropertyChanged += _testParameter_ItemPropertyChanged;
_testParameter.CollectionChanged += _testParameter_CollectionChanged;
UpdateParametersCommand = new RelayCommand(CallUpdateParameters);
}
private void _testParameter_ItemPropertyChanged(object sender, ItemPropertyChangedEventArgs<WrappedParameter> e)
{
Debug.WriteLine("PROPERTY CHANGE");
RaiseProperty(nameof(TestParameter));
int index = TestParameter.IndexOf(e.Item);
TestParameter[index] = e.Item;
}
private void _testParameter_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
Debug.WriteLine("COLLECTION CHANGE");
}
private void MakeRequest(RequestIdED request)
{
_handler.Request.Make(request);
_event.Raise();
}
private void CallUpdateParameters() { MakeRequest(RequestIdED.UpdateParameters); }
public void UpdateParameters()
{
Debug.WriteLine("Running Update Parameters");
try
{
using (var transaction = new Transaction(_doc))
{
transaction.Start("T_UpdateParameters");
foreach (WrappedParameter p in TestParameter)
{
string currenValue = p.RevitParameter.AsValueString();
if (p.RevitParameter.AsValueString() != p.ParamValue)
{
bool setValueSuccess = p.SetRevitParameterValue(p.ParamValue);
if(!setValueSuccess)
{
TaskDialog.Show("Parameter Value Not Set", "The parameter value for the parameter " + p.Name + " was not given a valid value and was not changed. Please ensure the units are correct.");
}
}
}
transaction.Commit();
}
}
catch (Exception e)
{
throw new Exception("Something Went Wrong. Check your values.");
}
}
public void UpdateParameterValues()
{
for(var i = 0; i < TestParameter.Count; i++)
{
TestParameter[i].UpdateValues();
}
}
}
C# Parameter Wrapper Class
public class TestParameter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
internal void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged(nameof(_name));
}
}
private string _categories;
public string Categories
{
get => _categories;
set
{
_categories = value;
OnPropertyChanged(nameof(_categories));
}
}
private string _paramValue;
public string ParamValue
{
get => _paramValue;
set
{
_paramValue = value;
OnPropertyChanged(nameof(_paramValue));
}
}
private Parameter _revitParameter;
public Parameter RevitParameter
{
get => _revitParameter;
set
{
_revitParameter = value;
OnPropertyChanged(nameof(_revitParameter));
}
}
private ElementId _elementId;
public ElementId ElementId
{
get => _elementId;
set
{
_elementId = value;
OnPropertyChanged(nameof(_elementId));
}
}
public TestParameter(Parameter param)
{
GetRevitParameterValue();
}
public void GetRevitParameterValue()
{
//Get parameter value logic
}
public bool SetRevitParameterValue(string Value)
{
//Set parameter value logic
}
}
C# TestObservableCollection Class
public class TestObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
public event EventHandler<ItemPropertyChangedEventArgs<T>> ItemPropertyChanged;
protected override void InsertItem(int index, T item)
{
base.InsertItem(index, item);
item.PropertyChanged += item_PropertyChanged;
}
protected override void RemoveItem(int index)
{
var item = this[index];
base.RemoveItem(index);
item.PropertyChanged -= item_PropertyChanged;
}
protected override void ClearItems()
{
foreach (var item in this)
{
item.PropertyChanged -= item_PropertyChanged;
}
base.ClearItems();
}
protected override void SetItem(int index, T item)
{
var oldItem = this[index];
oldItem.PropertyChanged -= item_PropertyChanged;
base.SetItem(index, item);
item.PropertyChanged += item_PropertyChanged;
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnItemPropertyChanged((T)sender, e.PropertyName);
}
private void OnItemPropertyChanged(T item, string propertyName)
{
var handler = this.ItemPropertyChanged;
if (handler != null)
{
handler(this, new ItemPropertyChangedEventArgs<T>(item, propertyName));
}
}
}
public sealed class ItemPropertyChangedEventArgs<T> : EventArgs
{
private readonly T _item;
private readonly string _propertyName;
public ItemPropertyChangedEventArgs(T item, string propertyName)
{
_item = item;
_propertyName = propertyName;
}
public T Item
{
get { return _item; }
}
public string PropertyName
{
get { return _propertyName; }
}
}
I have a WPF application that has multiple comboboxes and buttons. I am learning the MVVM model with this application. The first combobox will display a list of database instances. This is done at the start of the application. This works fine.
There is a button object next to the database instances combobox. When the user clicks this button I need to get the contents of the database instance combobox and use it in a call to get all the databases in that instance. I am using a RelayCommand (ICommand) for the actions. The action for the button is getting setup correctly. I have a method SelectedDatabase in the DBInstance class but it is null when I click the button.
In the LoadDBInfo method below the selectedItem parameter is null.
Here is my XAML:
<ComboBox x:Name="cbxRLFDBInstances" ItemsSource="{Binding DBInstances}"
SelectedValue="{Binding SelectedDBInstance}" SelectedValuePath="value"
HorizontalAlignment="Left" Height="28" Margin="189,87,0,0" VerticalAlignment="Top"
Width="250" FontFamily="Arial" FontSize="14.667"
IsEditable="True"/>
<Button x:Name="btnRLFDBLoadDBInfo" Content="Load DB Info" Command="{Binding LoadDBInfoCommand}"
CommandParameter="{Binding SelectedDBInstance}" HorizontalAlignment="Left" Height="26" Margin="475,89,0,0" VerticalAlignment="Top"
Width="101" FontFamily="Arial" FontSize="14.667" Background="#FFE8F9FF"
ToolTip="Click here after choosing or typing in the datbase instance. This will populate the database list."/>
<ComboBox x:Name="cbxRLFDBName" HorizontalAlignment="Left" Height="28" Margin="189,132,0,0"
ItemsSource="{Binding DBDatabases}" SelectedValue="{Binding SelectedDBDatabase}"
SelectedValuePath="value" VerticalAlignment="Top" Width="250" FontFamily="Arial"
FontSize="14.667" IsEditable="True" IsReadOnly="True"
ToolTip="Once a database is choosen the table list will automatically be populated."/>
Here is my ViewModel:
namespace DatabaseTest.ViewModel
{
class RLFDatabaseTableViewModel
{
Utilities dbtUtilities = new Utilities();
public RelayCommand LoadDBInfoCommand
{
get;
set;
}
public RLFDatabaseTableViewModel()
{
LoadDBInstances();
LoadDBInfoCommand = new RelayCommand(LoadDBInfo);
}
#region Database Instance
public IList<DBInstance> DBInstances
{
get;
set;
}
public void LoadDBInstances()
{
IList<DBInstance> dbInstances = nList<DBInstance>();
DataTable dt = SmoApplication.EnumAvailableSqlServers(false);
dbInstances.Add(new DBInstance { DBInstanceName = "fal-conversion\\mun2012ci" });
dbInstances.Add(new DBInstance { DBInstanceName = "fal-conversion\\mun2014ci" });
if (dt.Rows.Count > 0)
{
foreach (DataRow dr in dt.Rows)
{
dbInstances.Add(new DBInstance { DBInstanceName = dr["Name"].ToString() });
}
}
DBInstances = dbInstances;
}
#endregion Database Instance
#region Database Names
public IList<DBDatabase> DBDatabases
{
get;
set;
}
public void LoadDBDatabases()
{
IList<DBDatabase> dbDatabases = new List<DBDatabase>();
dbDatabases.Add(new DBDatabase { DBDatabaseName = "DB - A" });
dbDatabases.Add(new DBDatabase { DBDatabaseName = "DB - B" });
DBDatabases = dbDatabases;
}
#endregion Database Names
#region Button Cammands
void LoadDBInfo(object selectedItem)
{
SqlConnection sqlConn = null;
IList<DBDatabase> dbDatabaseNames = new List<DBDatabase>();
// string selectedItem = dbInstances.
//Setting the PUBLIC property 'TestText', so PropertyChanged event is fired
if (selectedItem == null)
dbDatabaseNames = null;
else
{
SelectedDBInstance = selectedItem as DBInstance;
dbDatabaseNames = dbtUtilities.GetDBNames(sqlConn, _selectedDBInstance.ToString(),
_selectedDBDatabase.ToString());
}
DBDatabases = dbDatabaseNames;
}
#endregion Button Commands
}
Here is my Model:
namespace DatabaseTest.Model
{
public class RLFDatabaseTableModel { }
public class DBInstance : INotifyPropertyChanged
{
private string strDBInstance;
public override string ToString()
{
return strDBInstance;
}
public string DBInstanceName
{
get
{
return strDBInstance;
}
set
{
if (strDBInstance != value)
{
strDBInstance = value;
RaisePropertyChanged("DBInstanceName");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
public class DBDatabase : INotifyPropertyChanged
{
private string strDBDatabase;
public override string ToString()
{
return strDBDatabase;
}
public string DBDatabaseName
{
get
{
return strDBDatabase;
}
set
{
if (strDBDatabase != value)
{
strDBDatabase = value;
RaisePropertyChanged("DBDatabaseName");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
EDIT: This is my code to load the 2nd combobox, cbxRLFDBName, The DBDatabase has the values but the combobox is not loaded.
public void LoadDatabases(string strDBInstanceName)
{
string strQuery;
IList<DBDatabase> dbDatabases = new List<DBDatabase>();
SqlConnection sqlUtilDBConn = null;
try
{
if (sqlUtilDBConn != null)
{
sqlUtilDBConn.Close();
}
sqlUtilDBConn = dbtUtilities.LoginToDatabase(strDBInstanceName, "master");
strQuery = "select name from sys.databases order by 1";
using (SqlCommand sqlCmd = new SqlCommand(strQuery, sqlUtilDBConn))
{
SqlDataReader sqlDataRead = sqlCmd.ExecuteReader();
while (sqlDataRead.Read())
{
string strDBNme = sqlDataRead.GetString(0);
dbDatabases.Add(new DBDatabase { DBDatabaseName = strDBNme });
}
sqlDataRead.Close();
sqlCmd.Dispose();
}
}
catch (Exception exQuery)
{
string strMsg;
strMsg = "GetNumRows: Error, '" + exQuery.Message + "', has occurred.";
System.Windows.MessageBox.Show(strMsg);
}
DBDatabases = dbDatabases;
}
EDIT: I have removed some of the code that is not needed in the hopes that this will be easier to read. My issue is that combobox "cbxRLFDBInstances" with ItemsSource="{Binding DBInstances}" loads the combobox fine. I also have another combobox, "cbxRLFDBName" with ItemsSource="{Binding DBDatabases}". When I choose the appropriate database instance and click the Load DB Info button, LoadDatabases runs and I can see that DBDatabases has the information needed in it. However the combobox is not loaded and I do not have a failure. Why does one ItemsSource data binding work and the other does not? I believe I am setting the class correctly but it seems lo=ike the binding is not happening? What have I missed?
Your code look fine to me, except for the SelectedValuePath="value" on the ComboBoxes. SelectedValuePath specifies a property on the selected item that is to be bound to the SelectedValue. SelectedDBInstance is of type DBInstance and DBInstance class does not define a value property, so I'd say you just have to remove SelectedValuePath="value" from the ComboBoxes.
Edit:
You need your ViewModel to implement INotifyPropertyChanged:
class RLFDatabaseTableViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
// the rest of RLFDatabaseTableViewModel implementation ...
}
And then every time you change a property value inside ViewModel, you also need to call RaisePropertyChanged immediately after. For example:
DBDatabases = dbDatabaseNames;
RaisePropertyChanged("DBDatabases");
It is helpful to define your properties like so:
public string StringProperty
{
get { return this.stringProperty; }
set {
this.stringProperty = value;
this.RaisePropertyChanged("StringProperty");
}
}
private string stringProperty;
Then you can just write
this.StringProperty = "new value";
and the new value will be set and a change notification sent.
You have to send the notifications because the View (XAML) and ViewModel are different classes and the View has no way of knowing that a property on the ViewModel has changed. If ViewModel implements INotifyPropertyChanged, WPF will listen for property changes through the PropertyChanged event and update the View accordingly.
Have you tried to pass command parameter as selected izem from combobox, something like:
CommandParameter="{Binding SelectedItem,ElementName=yourComboBoxName}"
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)
I have a listbox which has items populated from a database. Now I want to update a listbox with a new string value each time I call the Add function.
I did this in 2 ways.
I added the new value to the database and updated the ViewModel class where the listbox is binded to. And this works fine. (see AddNewNameFirstWay method below)
I added the new value to the database, reloaded the values from the database and updated the ViewModel. But this doesn't work. (see AddNewNameSecondWay method below)
Here is my ViewModel code
public class ViewModel
{
private DBContext context = new DBContext("Data source=isostore:/names2.sdf");
private ObservableCollection<NameTable> nameCollection;
public ObservableCollection<NameTable> NameCollection
{
get
{
return nameCollection;
}
set
{
if (nameCollection != value)
{
nameCollection = value;
NotifyPropertyChanged();
}
}
}
public void AddNewNameSecondWay(string s)
{
NameTable t = new NameTable() { Name = s };
context.NameDatabaseTable.InsertOnSubmit(t);
context.SubmitChanges();
LoadFromDB();
}
public void AddNewNameFirstWay(string s)
{
NameTable t = new NameTable() { Name = s };
context.NameDatabaseTable.InsertOnSubmit(t);
context.SubmitChanges();
NameCollection.Add(t);
}
public void LoadFromDB()
{
var query = from i in context.NameDatabaseTable
select i;
NameCollection = new ObservableCollection<NameTable>(query);
}
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName]string propertyName = null)
{
var tmp = PropertyChanged;
if (tmp != null)
tmp(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Here is my XAML binding code
<ListBox ItemsSource="{Binding NameCollection, Mode=OneWay}" >
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name, Mode=OneWay}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
It seems to me that second method doesn't work, because the ObservableCollection memory reference changes. If this is correct, how to update the binding properly?
Reason I use the second method is that, I want to make sure all the DB constraints stands true for the values I insert.
You need to notify about the changes in property. Try this way for Framework 4.0,
Mode=OneWayToSource,UpdateSourceTrigger=PropertyChanged
Hope it helps...
Call OnPropertyChanged when you want notification about changes in your collection:
OnPropertyChanged("NameCollection")
Your ViewModel:
public class ViewModel: INotifyPropertyChanged
{
private DBContext context = new DBContext("Data source=isostore:/names2.sdf");
private ObservableCollection<NameTable> nameCollection;
public ObservableCollection<NameTable> NameCollection
{
get
{
return nameCollection;
}
set
{
if (nameCollection != value)
{
nameCollection = value;
OnPropertyChanged("NameCollection");
}
}
}
public void AddNewNameSecondWay(string s)
{
// your code
OnPropertyChanged("NameCollection");
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I have some table ( using ListView ) and i want to show in the table some object ( make filter ) if some checkbox is checked .... ( if the checkbox is not checked i want to show all the table items )
How can i do it by using mvvm ? I can't use the .cs file that bihind the xaml.
Thanks
here is a little example how it can works
code at your xaml, bind to the FilterItems property
<CheckBox Content="should i filter the view?" IsChecked="{Binding FilterItems}" />
<ListView ItemsSource="{Binding YourCollView}" />
code behond at your model view
public class MainModelView : INotifyPropertyChanged
{
public MainModelView()
{
var coll = new ObservableCollection<YourClass>();
yourCollView = CollectionViewSource.GetDefaultView(coll);
yourCollView.Filter += new Predicate<object>(yourCollView_Filter);
}
bool yourCollView_Filter(object obj)
{
return FilterItems
? false // now filter your item
: true;
}
private ICollectionView yourCollView;
public ICollectionView YourCollView
{
get { return yourCollView; }
set
{
if (value == yourCollView) {
return;
}
yourCollView = value;
this.NotifyPropertyChanged("YourCollView");
}
}
private bool _filterItems;
public bool FilterItems
{
get { return _filterItems; }
set
{
if (value == _filterItems) {
return;
}
_filterItems = value;
// filer your collection here
YourCollView.Refresh();
this.NotifyPropertyChanged("FilterItems");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
var eh = PropertyChanged;
if (eh != null) {
eh(this, new PropertyChangedEventArgs(propertyName));
}
}
}
EDIT
a complete example is located here
hope that helps
You can bind IsChecked property of checkbox in ViewModel. And in prpoperty setter you can filter your collection which gets binded to ListView.
XAML:
<CheckBox IsChecked="{Binding IsChecked}"/>
ViewModel:
private bool _isChecked;
public bool IsChecked
{
get
{
return _isChecked;
}
set
{
_isChecked = value;
//filer your collection here
}
}