My interface displays a list of AD users, and a pair of buttons next to each name to disable/enable that user. Each button is bound to an appropriate DelegateCommand. Clicking the 'enable' button disables that button and enables the other one. Users could be getting enabled/disabled by other means by other users, and I'd like this to be reflected in my app.
What I've tried:
RaisePropertyChangedEvent("CommandEnable");
RaisePropertyChangedEvent("CommandDisable");
I'm using a MVVM.
I've got a Dispatch timer set to trigger the above lines every second.
EDIT: I seem to have solved this issue by updating the whole user list, and re-assigning it as the ItemSource for my ListBox.
I can't help but feel this is messy. Is there a way to only trigger an update for the users for which their enabled property has changed? In my Model
public class UserItem //The model
{
private UserPrincipal _User;
public string SamAccountName {
get
{
return _User.SamAccountName;
}
}
public UserItem(UserPrincipal user)
{
_User = user;
}
public bool Enabled
{
get
{
if (_User.Enabled != null)
{
return (bool)_User.Enabled;
} else
{
return false;
}
}
set
{
_User.Enabled = value;
_User.Save();
}
}
}
And in my ViewModel:
public class UserVM : ObservableObject, IComparable //ViewModel
{
private UserItem _userItem;
private ISet<String> _Groups = new HashSet<String>();
public string SamAccountName
{
get
{
return _userItem.SamAccountName;
}
}
public UserVM(UserPrincipal user, ISet<String> groups)
{
_userItem = new UserItem(user);
_Groups = groups;
}
public bool IsInGroup(String groupName)
{
return (_Groups.Contains(groupName)) ? true : false;
}
public bool Enabled
{
get
{
return _userItem.Enabled;
}
set
{
_userItem.Enabled = value;
RaisePropertyChangedEvent("Enabled");
RaisePropertyChangedEvent("CommandEnable");
RaisePropertyChangedEvent("CommandDisable");
}
}
private void EnableUser(object state)
{
Enabled = true;
}
private bool CanEnable(object state)
{
return !Enabled;
}
private void DisableUser(object state)
{
Enabled = false;
}
private bool CanDisable(object state)
{
return Enabled;
}
public int CompareTo(object comp)
{
return this._userItem.SamAccountName.CompareTo(((UserVM)comp).SamAccountName);
}
public ICommand CommandEnable
{
get {
return new DelegateCommand(EnableUser, CanEnable);
}
}
public ICommand CommandDisable
{
get { return new DelegateCommand(DisableUser, CanDisable); }
}
}
My XML:
<Window x:Class="Toggle.MainWindow"
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:Toggle"
mc:Ignorable="d"
Title="MainWindow" Height="512" Width="339">
<Window.Resources>
</Window.Resources>
<Grid Margin="10">
<ListBox Name="lbUserList" HorizontalContentAlignment="Stretch" VerticalAlignment="Stretch" Margin="0,22,0,0">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding SamAccountName}" />
<Button Name="btnEnable" Grid.Column="1" Content="Enable" Background="Green" Command="{Binding CommandEnable}"/>
<Button Name="btnDisable" Grid.Column="2" Content="Disable" Background="Red" Command="{Binding CommandDisable}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ComboBox Name="cbGroupList" SelectedIndex="0" DropDownClosed="ComboBox_DropDownClosed" HorizontalAlignment="Stretch" VerticalAlignment="Top" Width="Auto"/>
</Grid>
</Window>
Also, this polling is interfering with the user clicking of the button. If the UI update happens at the exact same time that the user clicks the button, the button click doesn't register. My gut tells me that this is a non-resolvable race condition (user-writer conflict), but I'm hopeful that I am wrong.
Related
I have a listview that can be filtered using a textbox:
<TextBox TextChanged="txtFilter_TextChanged" Name="FilterLv"/>
In the view code-behind I do the following:
CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(this.lv.ItemsSource);
view.Filter = UserFilter;
private bool UserFilter(object item)
{
if (String.IsNullOrEmpty(FilterLv.Text))
return true;
else
{
DataModel m = (item as DataModel);
bool result = (m.Name.IndexOf(Filter.Text, StringComparison.OrdinalIgnoreCase) >= 0 ||
//m.Surname.IndexOf(Filter.Text, StringComparison.OrdinalIgnoreCase) >= 0);
return result;
}
}
private void Filter_TextChanged(object sender, TextChangedEventArgs e)
{
CollectionViewSource.GetDefaultView(this.lv.ItemsSource).Refresh();
}
Now I have placed a label in the view and I would like this label to show the number of items currently displayed in the listview.
How can I do it? I have found things like this but I don't understand at all what is RowViewModelsCollectionView. In this link it is suggested to bind as below:
<Label Content="{Binding ModelView.RowViewModelsCollectionView.Count}"/>
Could anyone explain me or provide a very little and simple example on how to do it?
FINAL UPDATE:
View model:
public class TestViewModel
{
// lv is populated later in code
public ObservableCollection<DataModel> lv = new ObservableCollection<DataModel>();
public ObservableCollection<DataModel> LV
{
get
{
return this.lv;
}
private set
{
this.lv= value;
OnPropertyChanged("LV");
}
}
private CollectionView view;
public TestViewModel()
{
this.view = (CollectionView)CollectionViewSource.GetDefaultView(this.LV);
view.Filter = UserFilter;
}
private string textFilter;
public string TextFilter
{
get
{
return this.textFilter;
}
set
{
this.textFilter= value;
OnPropertyChanged("TextFilter");
if (String.IsNullOrEmpty(value))
this.view.Filter = null;
else
this.view.Filter = UserFilter;
}
}
private bool UserFilter(object item)
{
if (String.IsNullOrEmpty(this.TextFilter))
return true;
else
{
DataModel m = (item as DataModel);
bool result = (m.Name.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
//m.Surname.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0);
return result;
}
}
/// <summary>
/// Número de registros en la listview.
/// </summary>
public int NumberOfRecords
{
get
{
return this.view.Count;
}
}
}
View (xaml):
<!-- search textbox - filter -->
<TextBox TextChanged="txtFilter_TextChanged"
Text="{Binding TextFilter, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<!-- label to show the number of records -->
<Label Content="{Binding NumberOfRecords}"/>
view code-behind (xaml.cs):
private void txtFilter_TextChanged(object sender, TextChangedEventArgs e)
{
CollectionViewSource.GetDefaultView((DataContext as TestViewModel).LV).Refresh();
}
It is filtering ok when I type in the search textbox and listview is updated correctly but the number of records is always 0.
What am i doing wrong?
ATTEMPT2:
Below another attempt not working. If I attach my listivew to the View declared in model view then no items are shown. If I attach listview to LV in model view then items are shown, and when I filter through my search textbox it filters ok, listview is updated but the number of rows shown in the listview always remains to 0.
Notes:
I am using NET 3.5 Visual Studio 2008.
I need to set View as writable in model view because I do not set it
in view model constructor, instead i set it in LoadData method in
view model. LoadData is called from view code-behind constructor.
View Model:
namespace MyTest.Example
{
public Class TestViewModel : INotifyPropertyChanged // Implementations not here to simplify the code here.
{
private ObservableCollection<DataModel> lv;
public ObservableCollection<DataModel> LV
{
get
{
return this.lv;
}
private set
{
this.lv = value;
OnPropertyChanged("LV");
}
}
public CollectionView View { get; set; }
public TestViewModel()
{
this.LV = new ObservableCollection<DataModel>();
// this.View = (CollectionView)CollectionViewSource.GetDefaultView(this.LV);
// this.View.Filter = UserFilter;
}
private string textFilter = string.Empty;
public string TextFilter
{
get
{
return this.textFilter ;
}
set
{
this.textFilter = value;
OnPropertyChanged("TextFilter");
this.View.Refresh();
}
}
private bool UserFilter(object item)
{
if (String.IsNullOrEmpty(this.TextFilter))
return true;
else
{
DataModel m = (item as DataModel);
bool result = (m.Name.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
//m.Surname.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0);
return result;
}
}
public void LoadData()
{
this.LV = LoadDataFromDB();
this.View = (CollectionView)CollectionViewSource.GetDefaultView(this.LV);
this.View.Filter = UserFilter;
}
} // End Class
} // End namespace
View code-behing (xaml.cs):
namespace MyTest.Example
{
public Class TestView
{
public TestView()
{
InitializeComponent();
(DataContext as TestViewModel).LoadData();
}
}
}
View (xaml):
xmlns:vm="clr-namespace:MyTest.Example"
<!-- search textbox - filter -->
<TextBox Text="{Binding Path=TextFilter, UpdateSourceTrigger=PropertyChanged}">
<!-- label to show the number of records -->
<Label Content="{Binding Path=View.Count}" ContentStringFormat="No. Results: {0}"/>
<ListView Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Path=View}" SelectionMode="Extended" AlternationCount="2">
ATTEMPT 3:
Finally I have get it to work. Solution is the same as ATTEMPT2 but making below changes:
I have replaced this:
public CollectionView View { get; set; }
by this one:
private CollectionView view;
public CollectionView View {
get
{
return this.view;
}
private set
{
if (this.view == value)
{
return;
}
this.view = value;
OnPropertyChanged("View");
}
}
All the rest remains the same as in ATTEMPT2. In view View.Count and assigning View as ItemsSource to my listview now is working all perfectly.
You should use
<Label Content="{Binding ModelView.Count}"/>
instead of
<Label Content="{Binding ModelView.RowViewModelsCollectionView.Count}"/>
RowViewModelsCollectionView in the other question is the same as ModelView is in your case.
Edit
Count is a property from the CollectionView
For further information have a look at the MSDN
Edit 2
When you dont want to do it via XAML like in my example you have to implement INotifyPropertyChanged and raise this whenever the bound property is changed because otherwiese the UI won't get the change.
In your case: you have to call OnPropertyChanged("NumberOfRecords"); in your filter method. But it would be easier to do it via xaml like i Wrote earlier.
Here is a fully working example with the CollectionView in the view model, and the filter count automatically flowing to the bound control. It uses my mvvm library for the base ViewModel class to supply INotifyPropertyChanged, but you should easily be able to substitute your own system, I'm not doing anything special with it.
The full source code can be downloaded from here
XAML:
<Window
x:Class="FilterWithBindableCount.MainWindow"
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:local="clr-namespace:FilterWithBindableCount"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="525"
Height="350"
d:DataContext="{d:DesignInstance local:MainWindowVm}"
mc:Ignorable="d">
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Label
Grid.Row="0"
Grid.Column="0"
Margin="4">
Filter:
</Label>
<TextBox
Grid.Row="0"
Grid.Column="1"
Margin="4"
VerticalAlignment="Center"
Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="4"
Text="{Binding Path=PeopleView.Count, StringFormat={}Count: {0}}" />
<DataGrid
Grid.Row="3"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="4"
CanUserAddRows="False"
CanUserSortColumns="True"
ItemsSource="{Binding Path=PeopleView}" />
</Grid>
</Window>
View models:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using AgentOctal.WpfLib;
namespace FilterWithBindableCount
{
class MainWindowVm : ViewModel
{
public MainWindowVm()
{
People = new ObservableCollection<PersonVm>();
PeopleView = (CollectionView) CollectionViewSource.GetDefaultView(People);
PeopleView.Filter = obj =>
{
var person = (PersonVm)obj;
return person.FirstName.ToUpper().Contains(FilterText.ToUpper() ) || person.LastName.ToUpper().Contains(FilterText.ToUpper());
};
People.Add(new PersonVm() { FirstName = "Bradley", LastName = "Uffner" });
People.Add(new PersonVm() { FirstName = "Fred", LastName = "Flintstone" });
People.Add(new PersonVm() { FirstName = "Arnold", LastName = "Rimmer" });
People.Add(new PersonVm() { FirstName = "Jean-Luc", LastName = "Picard" });
People.Add(new PersonVm() { FirstName = "Poppa", LastName = "Smurf" });
}
public ObservableCollection<PersonVm> People { get; }
public CollectionView PeopleView { get; }
private string _filterText = "";
public string FilterText
{
get => _filterText;
set
{
if (SetValue(ref _filterText, value))
{
PeopleView.Refresh();
}
}
}
}
class PersonVm:ViewModel
{
private string _firstName;
public string FirstName
{
get {return _firstName;}
set {SetValue(ref _firstName, value);}
}
private string _lastName;
public string LastName
{
get {return _lastName;}
set {SetValue(ref _lastName, value);}
}
}
}
This is actually significantly easier when properly following MVVM. The CollectionView is either declared in the XAML, or as a property in the viewmodel. This allows you to bind directly to CollectionView.Count.
Here is an example of how to place the CollectionViewSource in XAML from one of my apps:
<UserControl
x:Class="ChronoPall.App.TimeEntryList.TimeEntryListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:app="clr-namespace:ChronoPall.App"
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ChronoPall.App.TimeEntryList"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DataContext="{d:DesignInstance local:TimeEntryListViewVm}"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<UserControl.Resources>
<CollectionViewSource x:Key="TimeEntriesSource" Source="{Binding Path=TimeEntries}">
<CollectionViewSource.SortDescriptions>
<componentModel:SortDescription Direction="Descending" PropertyName="StartTime.Date" />
<componentModel:SortDescription Direction="Ascending" PropertyName="StartTime" />
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="EntryDate" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</UserControl.Resources>
<Grid IsSharedSizeScope="True">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Source={StaticResource TimeEntriesSource}}">
<ItemsControl.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate DataType="{x:Type CollectionViewGroup}">
<local:TimeEntryListDayGroup />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ItemsControl.GroupStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:TimeEntryListItem />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>
It doesn't actually bind to Count, but it could easily do that with:
<TextBlock Text="{Binding Path=Count, Source={StaticResource TimeEntriesSource}}/>
To do it in the viewmodel, you would just create a readonly property of ICollectionView, and set it equal to CollectionViewSource.GetDefaultView(SomeObservableCollection), then bind to that.
Please help, I was trying to do this small example.
My aim is to when I keep the checkbox ticked the app should show the the Ip address of the Host I enter. But The checkbox IsChecked property is never updated in the view model, Even it is been changed in the UI
My View `
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.Background>
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.00" Color="LavenderBlush" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Grid.Background>
<StackPanel Grid.Row="0" Margin="150,30,69,236" Grid.ColumnSpan="2">
<TextBox x:Name="inputBox" Text="{Binding TxtHostName, Mode=TwoWay}" Foreground="Azure" Background="YellowGreen" VerticalAlignment="Bottom" Height="45"/>
</StackPanel>
<Button Command="{Binding StartCommand }" Content="Get IP" HorizontalAlignment="Left" Margin="257,89,0,0" VerticalAlignment="Top" Width="75" RenderTransformOrigin="0.013,-0.273"/>
<TextBlock Text="{Binding IpAddress}" Background="BlueViolet" Margin="150,153,69,104" Grid.ColumnSpan="2" />
<Button Content="Close" Command="{Binding CloseCommand}" HorizontalAlignment="Left" Margin="257,250,0,0" VerticalAlignment="Top" Width="75"/>
<CheckBox Content="CheckBox" IsChecked="{Binding IsSelected, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" HorizontalAlignment="Left" Margin="150,111,0,0" VerticalAlignment="Top"/>
</Grid>
`
My ViewModel:
public class ViewModel:INotifyPropertyChanged
{
#region INPC
public void RaisePropertyChanged(string propName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
private string txtHostName;
public string TxtHostName
{
get { return txtHostName; }
set { txtHostName = value;
RaisePropertyChanged("TxtHostName");
}
}
private string ipAddress;
public string IpAddress
{
get { return ipAddress; }
set { ipAddress = value;
RaisePropertyChanged("IpAddress");
}
}
private bool checkbox;
public bool CheckBox
{
get { return checkbox; }
set { checkbox = value;
RaisePropertyChanged("IsSelected");
}
}
public event EventHandler RequestClose;
protected void OnRequestClose()
{
if (RequestClose != null)
RequestClose(this, EventArgs.Empty);
}
private RelayCommand _StartCommand;
public ICommand StartCommand
{
get
{
if (this._StartCommand == null)
this._StartCommand = new RelayCommand(StartClick);
return this._StartCommand;
}
}
private RelayCommand _CloseCommand;
public ICommand CloseCommand
{
get
{
if(this._CloseCommand==null)
this._CloseCommand=new RelayCommand(CloseClick);
return this._CloseCommand;
}
}
private void CloseClick(object obj)
{
OnRequestClose();
}
private void StartClick(object obj)
{
if (checkbox)
{
string HostName = TxtHostName;
IPAddress[] ipaddress = Dns.GetHostAddresses(HostName);
foreach (IPAddress ipaddr in ipaddress)
{
IpAddress = ipaddr.ToString();
}
}
else
{
IpAddress = "Please tick the checkbox";
}
}
}
}
The RealyCommand is as it should be.
The CheckBox Property value never changes weather I change it in the UI or not.
Your raising your property changed event against IsSelected, but your bindable property is called Checkbox, rename Checkbox to IsSelected and update your private variable to something like isSelected.
In this case Id rename the variable to IsChecked or ComboBoxIsChecked.
I'm not sure if there is a copy-and-paste error but your View Model property is called Checkbox while you are raising the property changed event using the label IsSelected.
This and the error in the binding might be your problem. Based on your View Model the binding should be:-
<CheckBox Content="CheckBox" IsChecked="{Binding Checkbox, Mode=TwoWay}" HorizontalAlignment="Left" Margin="150,111,0,0" VerticalAlignment="Top"/>
Update: Recommendation if you are using C# 5.0 or above
To avoid typo's when creating setters and raising IPropertyNotifyChange events I would recommend using the CallerMemberName attribute as follows:-
public void RaisePropertyChanged([CallerMemberName] string propName = "")
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
Then your setter in your example becomes:-
private bool checkbox;
public bool CheckBox
{
get { return checkbox; }
set { checkbox = value;
RaisePropertyChanged();
}
}
Meaning as you refactor your View Model then the compiler will insert the name of the calling property to ensure the label in the INotifyProertyChanged event matches your property name without you having to remember to manually update it yourself.
My scenario is that there are two controls. One in which you set up minutes and second in which you specify seconds.
Both of them should be bound to single property in view model. This property is of type string. This string is in format [hh:mm:ss]. So changing value in "minutes" control should change 'mm' portion of the string and changing the value in "seconds" control should change the 'ss' portion of the string.
Thanks in advance
Here is a 3-property ViewModel working solution if you are using TimeSpan and its range is between 0 and 59h 59s. I have not fully tested and conditions/validation will change based on requirements. I used TimeSpan.TotalSeconds because that's the resolution we needed; meaning, when setting the TimeSpan to a new value, we would just set the total number of seconds through the public property. An alternative could be to have 2 TimeSpan properties in your ViewModel, then when setting the public property, you could call _item.TotalSeconds = VMMinutes.TotalSeconds + VMSeconds.TotalSeconds.TotalSeconds. Basically you have many design options here.
MainWindow.xaml:
<Grid>
<StackPanel>
<Border Height="60" BorderBrush="Black" BorderThickness="1">
<StackPanel Orientation="Horizontal">
<Label Content="Minutes"/>
<TextBox Text="{Binding Minutes}" />
<Label Content="Seconds"/>
<TextBox Text="{Binding Seconds}" />
</StackPanel>
</Border>
<Border Height="60" BorderBrush="Black" BorderThickness="1">
<StackPanel Orientation="Horizontal">
<Label Content="Total Seconds"/>
<TextBox Text="{Binding TotalSeconds}" />
</StackPanel>
</Border>
</StackPanel>
</Grid>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ItemViewModel(new Item(new TimeSpan(0, 3, 59)));
}
}
ItemViewModel.cs:
public class ItemViewModel : INotifyPropertyChanged
{
private readonly Item _item;
public event PropertyChangedEventHandler PropertyChanged;
public ItemViewModel(Item item)
{
_item = item;
}
public string TotalSeconds
{
get
{
return _item.TotalSeconds.ToString();
}
set
{
double newTotSecs;
if(!string.IsNullOrEmpty(value))
{
if(double.TryParse(value, out newTotSecs))
{
_item.TotalSeconds = newTotSecs;
NotifyPropertyChanged();
NotifyPropertyChanged("Minutes");
NotifyPropertyChanged("Seconds");
}
}
}
}
public string Seconds
{
get
{
return (_item.TotalSeconds % 60).ToString();
}
set
{
int newVal;
if(!string.IsNullOrEmpty(value))
{
if(int.TryParse(value, out newVal))
{
if(newVal >= 0 && newVal <= 59)
{
int totMinSec;
if(int.TryParse(Minutes, out totMinSec))
{
_item.TotalSeconds = (totMinSec * 60) + newVal;
NotifyPropertyChanged();
NotifyPropertyChanged("TotalSeconds");
}
}
}
}
}
}
public string Minutes
{
get
{
return ((int)(_item.TotalSeconds / 60)).ToString();
}
set
{
int newVal;
if(!string.IsNullOrEmpty(value))
{
if(int.TryParse(value, out newVal))
{
if(newVal >= 0 && newVal <= 59)
{
int totSec;
if(int.TryParse(Seconds, out totSec))
{
_item.TotalSeconds = totSec + (newVal * 60);
NotifyPropertyChanged();
NotifyPropertyChanged("TotalSeconds");
}
}
}
}
}
}
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Item.cs:
public class Item
{
private TimeSpan _time;
public double TotalSeconds
{
get
{
return _time.TotalSeconds;
}
set
{
if(value >= 0)
{
_time = new TimeSpan(0, 0, (int)value);
}
}
}
public Item(TimeSpan time)
{
_time = time;
}
}
Note: Your other option is to use a Converter, which I haven't provided a solution for. I think it could end up being cleaner in the long run since all you really need to pass to back and forth is the converter is total number of seconds.
I would use NETScape's approach above, but encapsulate it in a user control. The user control XAML would be something like:
<UserControl>
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Minutes" Grid.Row="0" Grid.Column="0"/>
<TextBox Text="{Binding InternalMinutes}" Grid.Row="0" Grid.Column="1"/>
<TextBlock Text="Seconds" Grid.Row="1" Grid.Column="0"/>
<TextBox Text="{Binding InternalSeconds}" Grid.Row="1" Grid.Column="1"/>
</Grid>
</UserControl>
Then in the code-behind, you would have a Dependency Property for the actual DateTime object, and properties to bind against (you could use a view model for this, or just go off of TextChanged. When its all View logic, its ok!).
An example property would be:
public int InternalSeconds
{
get { return ExternalTime.Seconds; }
set
{
ExternalTime.Seconds = value;
NotifyPropertyChanged();
}
}
Again, there are multiple approaches here, you could use a converter in order to use an intermediate object. ExternalTime is the DP here, make sure to handle its Changed event if you expect the value to change outside of this control.
I'm learning MVVM by write an litle app with Login function. In View layer, I have a LoginWindow with some binding like this:
<TextBox x:Name="tbxUsername" Grid.Row="0" Grid.Column="1" Width="150" Height="22" Margin="15,10,5,10"
Text="{Binding Path=Username, UpdateSourceTrigger=PropertyChanged}" />
<PasswordBox View:PasswordHelper.Attach="True" View:PasswordHelper.Password="{Binding Path=Password, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
x:Name="pwdPassword" Grid.Row="1" Grid.Column="1" Width="150" Height="22" Margin="15,10,5,10" />
The problem is i want to implement a binding like this:
<Window.Authenticated={Binding Path=Authenticated, UpdateSourceTrigger=PropertyChanged, Mode=OneWay, NotifyOnTargetUpdated=True} TargetUpdated="authenticated_TargetUpdated"/>
Authenticated is a bool value which will changed in my viewmodel.
Is there an way for me?
Edit for #lain:
Here my LoginWindow.xaml (style and layout removed).
<Window x:Class="ATCheck_View.LoginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:View="clr-namespace:ATCheck_View"
xmlns:ViewModel="clr-namespace:ATCheck_ViewModel;assembly=ATCheck_ViewModel"
Title="Login"
WindowStartupLocation="CenterScreen"
ResizeMode="CanMinimize"
SizeToContent="WidthAndHeight"
>
<Window.DataContext>
<ViewModel:LoginViewModel />
</Window.DataContext>
<Grid>
<TextBox x:Name="tbxUsername" Grid.Row="0" Grid.Column="1"
Width="150" Height="22" Margin="15,10,5,10" Text="{Binding Path=Username, UpdateSourceTrigger=PropertyChanged, TargetNullValue='atcheck', NotifyOnTargetUpdated=True}"/>
<PasswordBox View:PasswordHelper.Attach="True" View:PasswordHelper.Password="{Binding Path=Password, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, TargetNullValue='123456'}" x:Name="pwdPassword" Grid.Row="1" Grid.Column="1"
Width="150" Height="22" Margin="15,10,5,10" />
<Button x:Name="btnLogin" Width="65" Height="20" Margin="5,15,10,12"
Command="{Binding LoginCommand}"
CommandParameter="">
<TextBlock VerticalAlignment="Center">Login</TextBlock>
</Button>
<Button x:Name="btnCancel" Width="60" Height="20" Margin="5,15,5,12" Click="btnCancel_Click">
<TextBlock VerticalAlignment="Center">Cancel</TextBlock>
</Button>
</Grid>
</Window>
LoginViewModel:
public class LoginViewModel: ViewModelBase
{
private string _username;
private string _password;
private bool _authenticated = false;
public string Username
{
get
{
return _username;
}
set
{
_username = value;
RaisePropertyChangedEvent("Username");
}
}
public string Password
{
get
{
return _password;
}
set
{
_password = value;
RaisePropertyChangedEvent("Password");
}
}
public bool Authenticated
{
get
{
return _authenticated;
}
private set
{
_authenticated = value;
RaisePropertyChangedEvent("Authenticated");
}
}
public ICommand LoginCommand
{
get
{
return new RelayCommand<string>(Login);
}
}
private void Login(string p)
{
Authenticated = true;
Console.WriteLine("Infomation:");
Console.WriteLine(Authenticated);
Console.WriteLine(Username);
Console.WriteLine(Password);
}
}
ViewModelBase implement INotifyPropertyChanged and RelayCommand that I folow John Smith's article.
#nit: I tried as your lead, propdp, rebuild, and type Authenticated folow "Window" tag, but nothing happened when I press commbo Ctrl + Space.
What all i want to do is an messagebox that will show when Authenticated change from False to True:
private bool _authenticated = false;
public bool Authenticated
{
get
{
return _authenticated;
}
set
{
if (value == true)
{
MessageBox.Show("Logged!");
}
}
}
public LoginWindow()
{
InitializeComponent();
LoginViewModel myViewModel = (LoginViewModel)this.DataContext;
myViewModel.PropertyChanged += myViewModel_PropertyChanged;
}
void myViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "Authenticated")
{
Authenticated = ((LoginViewModel)sender).Authenticated;
}
}
You will have to add DependencyProperty to your Window class like below:
public static readonly DependencyProperty AuthenticatedProperty =
DependencyProperty.Register( "Authenticated", typeof(bool),
typeof(YOURWINDOWCLASS), new FrameworkPropertyMetadata(false));
// .NET Property wrapper
public bool Authenticated
{
get { return (bool)GetValue(AuthenticatedProperty ); }
set { SetValue(AuthenticatedProperty , value); }
}
Then you can bind
<Window Authenticated={Binding Path=Authenticated, UpdateSourceTrigger=PropertyChanged, Mode=OneWay, NotifyOnTargetUpdated=True} TargetUpdated="authenticated_TargetUpdated"/>
I've just stuck in a problem to bind collection in ItemsControl with ItemTeplate that contains bounded ComboBox.
In my scenario I need to "generate" form that includes textbox and combobox for each item in collection and let user to update items. I could use DataGrid for that but I'd like to see all rows in edit mode, so I use ItemsControl with custom ItemTemplate.
It's ok to edit textboxes but when you try to change any ComboBox, all other ComboBoxes in other rows will change too.
Is it a bug or feature?
Thanks, Ondrej
Window.xaml
<Window x:Class="ComboInItemsControlSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="480" Width="640">
<Window.Resources>
<CollectionViewSource x:Key="cvsComboSource"
Source="{Binding Path=AvailableItemTypes}" />
<DataTemplate x:Key="ItemTemplate">
<Border BorderBrush="Black" BorderThickness="0.5" Margin="2">
<Grid Margin="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Path=ItemValue}" />
<ComboBox Grid.Column="2"
SelectedValue="{Binding Path=ItemType}"
ItemsSource="{Binding Source={StaticResource cvsComboSource}}"
DisplayMemberPath="Name"
SelectedValuePath="Value" />
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Path=SampleItems}"
ItemTemplate="{StaticResource ItemTemplate}"
Margin="10" />
</Grid>
Window.xaml.cs
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
public class ViewModel
{
public ViewModel()
{
SampleItems = new List<SampleItem> {
new SampleItem { ItemValue = "Value 1" },
new SampleItem { ItemValue = "Value 2" },
new SampleItem { ItemValue = "Value 3" }
};
AvailableItemTypes = new List<SampleItemType> {
new SampleItemType { Name = "Type 1", Value = 1 },
new SampleItemType { Name = "Type 2", Value = 2 },
new SampleItemType { Name = "Type 3", Value = 3 },
new SampleItemType { Name = "Type 4", Value = 4 }
};
}
public IList<SampleItem> SampleItems { get; private set; }
public IList<SampleItemType> AvailableItemTypes { get; private set; }
}
public class SampleItem : ObservableObject
{
private string _itemValue;
private int _itemType;
public string ItemValue
{
get { return _itemValue; }
set { _itemValue = value; RaisePropertyChanged("ItemValue"); }
}
public int ItemType
{
get { return _itemType; }
set { _itemType = value; RaisePropertyChanged("ItemType"); }
}
}
public class SampleItemType : ObservableObject
{
private string _name;
private int _value;
public string Name
{
get { return _name; }
set { _name = value; RaisePropertyChanged("Name"); }
}
public int Value
{
get { return _value; }
set { _value = value; RaisePropertyChanged("Value"); }
}
}
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName) {
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Picture
here you can see the result on picture
I believe it's because you're binding to a CollectionViewSource, which tracks the current item. Try binding directly to your list instead, which won't track the current item
<ComboBox Grid.Column="2"
SelectedValue="{Binding Path=ItemType}"
DisplayMemberPath="Name"
SelectedValuePath="Value"
ItemsSource="{Binding RelativeSource={
RelativeSource AncestorType={x:Type ItemsControl}},
Path=DataContext.AvailableItemTypes}" />
While you have a combobox in each row, it doesnt see these comboboxes as being seperate. i.e. They are all using the same collection, and the same selectedValue, so when a value changes in one box, it changes in all of them.
The best way to fix this is to add the SampleItemType collection as a property on your SampleItem model and to then bind the combo box to that property.