I'm developing a UWP App, with Mvvm Light and Behaviours SDK. I defined a multi selectable ListView:
<ListView
x:Name="MembersToInviteList"
IsMultiSelectCheckBoxEnabled="True"
SelectionMode="Multiple"
ItemsSource="{Binding Contacts}"
ItemTemplate="{StaticResource MemberTemplate}">
</ListView>
I'd like, with a button binded to a MVVM-Light RelayCommand, to obtain a list with the selected items:
<Button
Command="{Binding AddMembersToEvent}"
CommandParameter="{Binding ElementName=MembersToInviteList, Path=SelectedItems}"
Content="Ok"/>
The RelayCommand (of MVVM-Light framework):
private RelayCommand<object> _addMembersToEvent;
public RelayCommand<object> AddMembersToEvent
{
get
{
return _addMembersToEvent
?? (_addMembersToEvent = new RelayCommand<object>(
(selectedMembers) =>
{
// Test
// selectedMembers is always null!
}));
}
}
I put a breakpoint inside the command, and I notice that selectedMembers is always null, although I select various items. By the console output I don't see any binding error or something else.
Also, if I pass as CommandParameter the whole list, and I put a breakpoint inside command's definition, i notice that I can't access to SelectedItems nor SelecteRanges value.
<DataTemplate x:Name="MemberTemplate">
<Viewbox MaxWidth="250">
<Grid Width="250"
Margin="5, 5, 5, 5"
Background="{StaticResource MyLightGray}"
BorderBrush="{StaticResource ShadowColor}"
BorderThickness="0, 0, 0, 1"
CornerRadius="4"
Padding="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0"
Width="45"
Height="45"
Margin="5,0,5,0"
VerticalAlignment="Center"
CornerRadius="50">
<Grid.Background>
<ImageBrush AlignmentX="Center"
AlignmentY="Center"
ImageSource="{Binding Image.Url,
Converter={StaticResource NullGroupImagePlaceholderConverter}}"
Stretch="UniformToFill" />
</Grid.Background>
</Grid>
<TextBlock Grid.Column="1"
Margin="3"
VerticalAlignment="Center"
Foreground="{StaticResource ForegroundTextOverBodyColor}"
Style="{StaticResource LightText}"
Text="{Binding Alias}" />
</Grid>
</Viewbox>
</DataTemplate>
What's the reason? How can I obtain such list?
One of the solutions to pass SelectedItems from ListView in ViewModel (with RelayCommands) is described in igralli's blog.
Pass ListView SelectedItems to ViewModel in Universal apps
Try the following code to get the selected objects from the parameter.
private RelayCommand<IList<object>> _addMembersToEvent;
public RelayCommand<IList<object>> AddMembersToEvent
{
get
{
return _addMembersToEvent
?? (_addMembersToEvent = new RelayCommand<IList<object>>(
selectedMembers =>
{
List<object> membersList = selectedMembers.ToList();
}));
}
}
I've made a small example for your case without MVVM-Light and it works perfect.
Maybe the problem is within the RelayCommand-class. I've written the following:
public class RelayCommand<T> : ICommand
{
private readonly Action<T> execute;
private readonly Predicate<T> canExecute;
public RelayCommand(Action<T> execute, Predicate<T> canExecute = null )
{
if (execute == null)
throw new ArgumentNullException("execute");
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (canExecute == null)
return true;
return canExecute((T) parameter);
}
public void Execute(object parameter)
{
execute((T) parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
Thanks to Roman's answer I figured out how to solve the issue:
First of all, as Roman suggested:
private RelayCommand<IList<object>> _addMembersToEvent;
public RelayCommand<IList<object>> AddMembersToEvent
{
get
{
return _addMembersToEvent
?? (_addMembersToEvent = new RelayCommand<IList<object>>(
selectedMembers =>
{
List<object> membersList = selectedMembers.ToList();
}));
}
}
Then, the XAML:
<Button
Command="{Binding AddMembersToEvent}"
CommandParameter="{Binding ElementName=MembersToInviteList, Converter={StaticResource ListViewSelectedItemsConverter}}"
Content="Ok"/>
The difference here is that I passed the whole list as parameter, not it's SelectedItems property. Then, using an IValueConverter I converted from the ListView object to IList<object> of SelectedMember:
public class ListViewSelectedItemsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
var listView = value as ListView;
return listView.SelectedItems;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
In this way the RelayCommand<IList<object>> got the right list and not a null value.
I can't find a reason but you can easily skirt the issue altogether by having an "IsSelected" property on your Contact object and putting a check box on your template:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Contacts}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected, Mode=TwoWay}"></CheckBox>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBlock Grid.Row="1" Text="{Binding SelectedItemsOutput}"></TextBlock>
<Button Grid.Row="2" Content="What is checked?" Command="{Binding GoCommand}"></Button>
</Grid>
and VMs etc:
public class MainViewModel : INotifyPropertyChanged
{
private string _selectedItemsOutput;
public ObservableCollection<Contact> Contacts { get; set; } = new ObservableCollection<Contact> { new Contact() { Id = 1, Name = "Foo" }, new Contact() { Id = 2, Name = "Bar" } };
public ICommand GoCommand => new RelayCommand(Go);
public string SelectedItemsOutput
{
get { return _selectedItemsOutput; }
set
{
if (value == _selectedItemsOutput) return;
_selectedItemsOutput = value;
OnPropertyChanged();
}
}
void Go()
{
SelectedItemsOutput = string.Join(", ", Contacts.Where(x => x.IsSelected).Select(x => x.Name));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Contact : INotifyPropertyChanged
{
private bool _isSelected;
public int Id { get; set; }
public string Name { get; set; }
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value == _isSelected) return;
_isSelected = value;
OnPropertyChanged();
}
}
public override string ToString()
{
return Name;
}
public event PropertyChangedEventHandler PropertyChanged;
}
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
DataContext = new MainViewModel();
}
}
Just my five cents and might be an absolutely long shot, but you should check this:
If the ItemsSource implements IItemsRangeInfo, the SelectedItems collection is not updated based on selection in the list. Use the SelectedRanges property instead.
I am just guessing based on Tomtom's answer, since he says he did almost exactly as you and got a working solution. Maybe the difference is in this little detail. I haven't checked it myself yet.
Related
So I have this project where I have two buttons and a ListView. The ListView is separated into it's own UserControl with it's own ViewModel which contains a ObservableCollection.
I'm using a ContentPresenter to display that control because I will be using different views.
Currently, when I'm clicking the Log button, it does in fact add the string to the collection, but the view doesn't update. And it keeps adding more and more everytime I click on it. (I've put a breakpoint inside private void AddItemOne() to inspect it to prove that it adds items.)
Question
Why doesn't my view update when I click the "Log" button even though it's adding items.
It does add the first item if I hardcode it like this.
public LogViewModel()
{
Logs = new ObservableCollection<string>();
Logs.Add("Test");
}
MainWindow.xaml
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Grid.Column="0" Height="25" Content="Log"
Command="{Binding AddItemOneCommand}"/>
<Button Grid.Row="0" Grid.Column="1" Height="25" Content="Other"
Command="{Binding AddItemTwoCommand}"/>
<UserControl Content="{Binding CurrentView}" Grid.Row="1" Grid.ColumnSpan="2"/>
</Grid>
MainViewModel.cs
class MainViewModel
{
public RelayCommand AddItemOneCommand { get; set; }
public RelayCommand AddItemTwoCommand { get; set; }
private object _currentView;
public object CurrentView
{
get { return _currentView; }
set
{
_currentView = value;
}
}
/* ViewModels */
public LogViewModel LogViewModel { get; set; }
public MainViewModel()
{
AddItemOneCommand = new RelayCommand(o => AddItemOne());
AddItemTwoCommand = new RelayCommand(o => AddItemTwo());
LogViewModel = new LogViewModel();
_currentView = LogViewModel;
}
private void AddItemOne()
{
LogViewModel.Logs.Add("Test");
}
private void AddItemTwo()
{
LogViewModel.Logs.Add("Test");
}
}
LogView.xaml
<UserControl.DataContext>
<local:LogViewModel/>
</UserControl.DataContext>
<Grid>
<ListView ItemsSource="{Binding Logs}" Background="Gray"/>
</Grid>
LogViewModel.cs
class LogViewModel
{
public ObservableCollection<string> Logs { get; set; }
public LogViewModel()
{
Logs = new ObservableCollection<string>();
}
}
Misc
App.xaml
<Application.Resources>
<DataTemplate DataType="{x:Type local:LogViewModel}">
<local:LogView/>
</DataTemplate>
</Application.Resources>
And the RelayCommand
public class RelayCommand : ICommand
{
private Action<object> execute;
private Func<object, bool> canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return this.canExecute == null || this.canExecute(parameter);
}
public void Execute(object parameter)
{
this.execute(parameter);
}
}
Remove this from LogView.xaml:
<UserControl.DataContext>
<local:LogViewModel/>
</UserControl.DataContext>
It creates another instance of the LogViewModel instead of using the one that you create in the MainViewModel.
You should also replace the UserControl in MainWindow.xaml with a ContentControl that binds to the CurrentView property:
<ContentControl Content="{Binding CurrentView}" Grid.Row="1" Grid.ColumnSpan="2" />
I'm relatively new to MVVM and I am trying to follow the principles. I have an issue where the Model is updated externally. The model then raises an event and the ViewModel handles it, updates the relevant fields. The appropriate fields then raise the onPropertyChange event. However the UI is not updating. If I reload the page the view is updated. My Xaml:
<ItemsControl ItemsSource="{Binding EBBusBreakers}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="BlanchedAlmond" CornerRadius="5" Margin="0.5" BorderBrush="Black" BorderThickness="1" Padding="1">
<Border.Resources>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="Text" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Button">
<Setter Property="Margin" Value="1" />
<Setter Property="FontSize" Value="12"/>
<Setter Property="Padding" Value="0"/>
</Style>
</Border.Resources>
<Grid Margin="0">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" HorizontalAlignment="Left" Grid.Column="0"/>
<TextBlock FontSize="12" HorizontalAlignment="Center" Grid.Row="0" Grid.Column="1" >
<TextBlock.Text>
<Binding Path="Status" PresentationTraceSources.TraceLevel="High" />
</TextBlock.Text>
</TextBlock>
<TextBlock Text="{Binding ApplicationName}" Grid.Row="1" Grid.Column="0" />
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="On" Command="{Binding TurnOnCommand}" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
<Button Grid.Column="1" Content="Off" Command="{Binding TurnOffCommand}" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
<Button Grid.Column="2" Content="Reset" Command="{Binding ResetCommand}" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
</Grid>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="3" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
My ViewModel:
public class BreakerCollectionViewModel {
private ObservableCollection<IBreakerNodeControl> _ebBusBreakers;
public ObservableCollection<IBreakerNodeControl> EBBusBreakers
{
get => _ebBusBreakers;
set => SetProperty(ref _ebBusBreakers, value);
}
}
public class BreakerViewModel : BaseViewModel, IBreakerNodeControl
{
private readonly S201Breaker _breaker;
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private string _status;
public string Status
{
get => _status;
set => SetProperty(ref _status, value);
}
public string ApplicationName { get; set; }
public RelayCommand TurnOnCommand => new RelayCommand(param => TurnOn());
public RelayCommand TurnOffCommand => new RelayCommand(param => TurnOff());
public RelayCommand ResetCommand => new RelayCommand(param => Reset());
public BreakerViewModel(S201Breaker breaker)
{
Name = breaker.Name;
_breaker = breaker;
//Bind to property changed event to update status
_breaker.PropertyChanged += UpdateState;
}
private void UpdateState(object sender, PropertyChangedEventArgs args)
{
if (sender is S201Breaker breaker)
{
Status = breaker.State.ToString();
}
}
public void TurnOn()
{
_breaker.DesiredState = BreakerTargetStateEnum.On;
}
public void TurnOff()
{
_breaker.DesiredState = BreakerTargetStateEnum.Off;
}
public void Reset()
{
_breaker.DesiredState = BreakerTargetStateEnum.Reset;
}
}
My Model:
public class S201Breaker : INotifyPropertyChanged
{
public string Name { get; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyThatChanged = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyThatChanged));
}
private BreakerStateEnum _state;
public BreakerStateEnum State
{
get => _state;
set
{
_state = value;
OnPropertyChanged();
}
}
protected S201Breaker(string name)
{
Name = name;
State = BreakerStateEnum.Unknown;
}
}
EDIT: My BaseViewModel (SetProperty):
public class BaseViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
public bool SetProperty<T>(ref T property, T value, [CallerMemberName] string name = "")
{
if (EqualityComparer<T>.Default.Equals(property, value))
{
return false;
}
property = value;
OnPropertyChanged(name);
return true;
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private readonly Dictionary<string, List<string>> _errorsByPropertyName = new Dictionary<string, List<string>>();
public IEnumerable GetErrors(string propertyName)
{
if (propertyName == null)
{
var allPropertyErrors = new List<string>();
foreach (var keyValuePair in _errorsByPropertyName)
{
keyValuePair.Value.ForEach(error => allPropertyErrors.Add(error));
}
return allPropertyErrors;
}
else
{
return _errorsByPropertyName.ContainsKey(propertyName) ? _errorsByPropertyName[propertyName] : null;
}
}
public bool HasErrors => _errorsByPropertyName.Any();
protected void AddError(string error, [CallerMemberName] string propertyName = null)
{
if (!_errorsByPropertyName.ContainsKey(propertyName))
{
_errorsByPropertyName[propertyName] = new List<string>();
}
if (_errorsByPropertyName[propertyName].Contains(error))
{
return;
}
_errorsByPropertyName[propertyName].Add(error);
OnErrorsChanged(propertyName);
}
protected void ClearErrors([CallerMemberName] string propertyName = null)
{
if (!_errorsByPropertyName.ContainsKey(propertyName))
{
return;
}
_errorsByPropertyName.Remove(propertyName);
OnErrorsChanged(propertyName);
}
private void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
#region Events
public event PropertyChangedEventHandler PropertyChanged;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
#endregion
}
EDIT: Adding interface code
public interface IBreakerNodeControl
{
string Name { get; }
string Status { get; }
string ApplicationName { get; set; }
RelayCommand TurnOnCommand { get; }
RelayCommand TurnOffCommand { get; }
RelayCommand ResetCommand { get; }
}
I have simplified the xaml and class though it should show what I am trying (rightly or wrongly) to achieve.
I have tried the following:
Removing the collection and updating just the one instance
Following the events (looks like it should be updating as normal)
Setting the tracelevel to high (this shows that the event is on triggered. Actual message: Got PropertyChanged event from BreakerViewModel)
Changing the updateSourceTrigger to onPropertyChange
Setting binding mode to oneWay
Implementing INorifyPropertyChanged in IBreakerNodeControl
I am aware that these are "simple" fixes. I am unsure though what the problem is as I can step from the model change through to the viewmodel and the viewmodel raises the event (as shown by the trace).
If there are any issues please let me know and I will amend my question to help. Any improvements are welcomed as it will help me improve!
TL;DR: Implemented INotifyPropertyChanged, change the property and the ui doesnt update until the page is refreshed even though trace says event is received.
Dicussion and prodding from Clemens in the comments made me realise that I am updating the models and subsequently the viewmodels within a task. After a bit of research I came up with the following solution by syncing UI contexts.
private void ProcessMessageQueue(TaskScheduler uiContext)
{
while (!CancelToken.IsCancellationRequested)
{
ResponseResetSignal.WaitOne();
while (CommsToLogicQueue.TryDequeue(out var incomingMessage))
{
var result = incomingMessage;
Task.Factory.StartNew(() => ProcessMessage(result), CancelToken, TaskCreationOptions.None, uiContext);
}
}
}
I'm new to UWP I am attempting to bind to an event in my ViewModel from a button flyout inside a listview that is shown on every item. I've looked at many solutions online and came up with the following code, it compiles fine but when I click the said Edit button nothing happens.
My ViewModel is available from the Page's context and not the Item's context
XAML
<ListView x:Name="MainListView"
ItemsSource="{x:Bind ViewModel.Devices, Mode=OneWay}"
SelectionMode="Multiple"
SelectionChanged="MainListView_SelectionChanged">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Width="Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0*"></ColumnDefinition>
<ColumnDefinition Width=".4*"></ColumnDefinition>
<ColumnDefinition Width="3*"></ColumnDefinition>
<ColumnDefinition Width="3*"></ColumnDefinition>
<ColumnDefinition Width="3*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="2" Text="{Binding AssetNumber}"/>
<TextBlock Grid.Column="3" Text="{Binding SerialNumber}"/>
<TextBlock Grid.Column="4" Text="{Binding Model}"/>
<Button Grid.Column="1" Height="30" Width="30">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem Text="Edit" Icon="Edit"
Command="{Binding ElementName=MainListView,Path=DataContext.ViewModel.EditCommand}"
CommandParameter="{Binding}"/>
</MenuFlyout>
</Button.Flyout>
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
View Model Class
public class MainPageViewModel
{
// Elements contained in the main listview
public ObservableCollection<Device> Devices = new ObservableCollection<Device>();
public MainPageViewModel()
{
DeviceProvider.Fill(ref Devices, 100);
EditCommand = new RelayCommand<Device>(EditDevice);
}
public RelayCommand<Device> EditCommand { get; set; }
private async void EditDevice(Device device)
{
// Code here that creates a dialog
}
}
The Device class
public class Device : INotifyPropertyChanged
{
private string assetNumber;
private string serialNumber;
private string model;
public string AssetNumber
{
get
{
return assetNumber;
}
set
{
assetNumber = value;
OnPropertyChanged();
}
}
public string SerialNumber
{
get
{
return serialNumber;
}
set
{
serialNumber = value;
OnPropertyChanged();
}
}
public string Model
{
get
{
return model;
}
set
{
model = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
The RelayCommand class
public class RelayCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<bool> _canExecute;
public event EventHandler CanExecuteChanged;
public RelayCommand(Action<T> execute) : this(execute, null)
{
}
public RelayCommand(Action<T> execute, Func<bool> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute();
}
public void Execute(object parameter)
{
_execute((T)parameter);
}
public void RaiseCanExecuteChanged()
{
var handler = CanExecuteChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
}
Your code doesn't seem to have any problems. So it should work perfectly. But if not, I'd suspect MainPage.ViewModel member might not be defined properly. The property to be used in {Binding} must be "public" and must have "get" accessor.
public sealed partial class MainPage : Page
{
public MainPageViewModel ViewModel { get; set; } = new MainPageViewModel();
public MainPage()
{
this.InitializeComponent();
DataContext = this;
}
}
it compiles fine but when I click the said Edit button nothing happens.
The problem is that you bind wrong Path(Path=DataContext.ViewModel.EditCommand) for MenuFlyoutItem, please remove ViewModel field. And I have edited your code please refer the following.
<Page.DataContext>
<local:MainPageViewModel x:Name="ViewModel"/>
</Page.DataContext>
......
<Button
Grid.Column="1"
Width="30"
Height="30"
>
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
Command="{Binding ElementName=MainListView, Path=DataContext.EditCommand}"
CommandParameter="{Binding}"
Icon="Edit"
Text="Edit"
/>
</MenuFlyout>
</Button.Flyout>
</Button>
I have a DataGrid which is bound to a ViewModel. When I select a record from the DataGrid, the TextBoxes (Username and Role) are displaying the data from the selected record.
I want to edit the selected record but I'd like to check the data before it updates the list, hence the 'OneWay' binding mode.
I'm having trouble passing the values of the textboxes to the view model. I can get a value of one textboxes through the button and passing the value to my ICommand
<Button Grid.Row="5" Grid.Column="1" Content="Edit" Margin="5 5"
Command="{Binding EditUserCmd, Source={StaticResource viewModelUsers}}" CommandParameter="{Binding Text, ElementName=txtUsername}
Is there a way to pass all the textboxes to the view model by creating a property in it that holds selected user? or passing the values of the texboxes to the view model somehow??
Thanks.
My view model
public class UsersViewModel
{
public ObservableCollection<UsersModel> Users { get; set; }
private ICommand addUserCommand;
private ICommand removeUserCommand;
private ICommand editUserCommand;
public ICommand AddUserCmd => addUserCommand ?? (addUserCommand = new AddUserCommand(this));
public ICommand RemoveUserCmd => removeUserCommand ?? (removeUserCommand = new DeleteUserCommand(this));
public ICommand EditUserCmd => editUserCommand ?? (editUserCommand = new EditUserCommand(this));
private UsersModel selectedUser = new UsersModel();
public UsersModel SelectedUser
{
get { return this.selectedUser; }
set
{
this.selectedUser = value;
}
}
public UsersViewModel()
{
// fetch data from db.
DataAccess da = new DataAccess();
Users = new ObservableCollection<UsersModel>(da.GetRegisteredUsers());
}
}
Model
public class UsersModel
{
public int Id { get; set; }
public string Username { get; set; }
public string Surname {get; set;}
}
Edit Command
internal class EditUserCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public UsersViewModel UsersViewModel { get; set; }
public EditUserCommand(UsersViewModel usersViewModel)
{
this.UsersViewModel = usersViewModel;
}
public bool CanExecute(object parameter)
{
// UsersModel user = (UsersModel)parameter;
// if (user != null)
//return !string.IsNullOrEmpty(user.Id.ToString());
return true;
}
public void Execute(object parameter)
{
// UsersModel user = (UsersModel)parameter;
// if (user != null)
// this.UsersViewModel.Users
}
}
xaml
...
<Window.Resources>[enter image description here][1]
<m:UsersModel x:Key="users"></m:UsersModel>
<vm:UsersViewModel x:Key="viewModelUsers"/>
</Windows.Resources>
...
<DataGrid x:Name="gridUsers"
Grid.Row="0"
DataContext="{Binding Source={StaticResource viewModelUsers}}" CanUserAddRows="False"
ItemsSource="{Binding Users}">
</DataGrid>
<Grid Margin="10" Grid.Row="1" DataContext="{Binding ElementName=gridUsers, Path=SelectedItem}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0">UserName:</Label>
<TextBox x:Name="txtUsername" Grid.Row="0" Grid.Column="1" Margin="0,0,0,10" Text="{Binding Path=Username, Mode=OneWay}"/>
<Label Grid.Row="1">Role:</Label>
<TextBox x:Name="txtRole" Grid.Row="1" Grid.Column="1" Margin="0,0,0,10" Text="{Binding Path=Role, Mode=OneWay}"/>
<StackPanel Grid.Row="5" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Center">
<Button Grid.Row="5" Grid.Column="1" Content="Edit" Margin="5 5"
Command="{Binding EditUserCmd, Source={StaticResource viewModelUsers}}" CommandParameter="{Binding Text, ElementName=txtUsername}">
</StackPanel>
</Grid>
Your ViewModel should not know about text boxes - just add two new properties ([PropertyName]EditValue) and bind to them, then in your command check them and copy them to the model if correct or restore them if incorrect - This is the entire point in using view models instead of binding to models directly
Did you know that you can edit the DataGrid cells directly? You can even use data validation. This way invalid data cells get a red border and the data won't be committed unless the validation passes.
Another option is to let UsersModel implement INotifyDataErrorInfo and validate properties directly. Then bind the DataGrid.SelectedItem to the view model and bind the edit TextBox elements to this property. This way you implemented live update and got rid of the edit commands:
UsersViewModel.cs
public class UsersViewModel
{
public ObservableCollection<UsersModel> Users { get; set; }
private UsersModel selectedUser;
public UsersModel SelectedUser
{
get => this.selectedUser;
set => this.selectedUser = value;
}
public UsersViewModel()
{
// fetch data from db.
DataAccess da = new DataAccess();
Users = new ObservableCollection<UsersModel>(da.GetRegisteredUsers());
}
}
UsersModel.cs
public class UsersModel : INotifyDataErrorInfo
{
private int id;
public int Id
{
get => this.id;
set { if (this.id != value && IsIdValid(value)) this.id = value; }
}
private string userName;
public string UserName
{
get => this.userName;
set { if (this.userName != value && IsUserNameValid(value) && ) this.userName = value; }
}
private string surname;
public string Surname
{
get => this.surname;
set { if (this.surname != value && IsSurnameValid(value) && ) this.surname = value; }
}
// Validates the Id property, updating the errors collection as needed.
public bool IsIdValid(int value)
{
RemoveError(nameof(this.Id), ID_ERROR);
if (value < 0)
{
AddError(nameof(this.Id), ID_ERROR, false);
return false;
}
return true;
}
public bool IsUserNameValid(string value)
{
RemoveError(nameof(this.UserName), USER_NAME_ERROR);
if (string.IsNullOrWhiteSpace(value))
{
AddError(nameof(this.UserName), USER_NAME_ERROR, false);
return false;
}
return true;
}
public bool IsSurnameValid(string value)
{
RemoveError(nameof(this.Surname), SURNAME_ERROR);
if (string.IsNullOrWhiteSpace(value))
{
AddError(nameof(this.Surname), SURNAME_ERROR, false);
return false;
}
return true;
}
private Dictionary<String, List<String>> errors =
new Dictionary<string, List<string>>();
private const string ID_ERROR = "Value cannot be less than 0.";
private const string USER_NAME_ERROR = "Value cannot be empty.";
private const string SURNAME_ERROR = "Value cannot be empty.";
// Adds the specified error to the errors collection if it is not
// already present, inserting it in the first position if isWarning is
// false. Raises the ErrorsChanged event if the collection changes.
public void AddError(string propertyName, string error, bool isWarning)
{
if (!errors.ContainsKey(propertyName))
errors[propertyName] = new List<string>();
if (!errors[propertyName].Contains(error))
{
if (isWarning) errors[propertyName].Add(error);
else errors[propertyName].Insert(0, error);
RaiseErrorsChanged(propertyName);
}
}
// Removes the specified error from the errors collection if it is
// present. Raises the ErrorsChanged event if the collection changes.
public void RemoveError(string propertyName, string error)
{
if (errors.ContainsKey(propertyName) &&
errors[propertyName].Contains(error))
{
errors[propertyName].Remove(error);
if (errors[propertyName].Count == 0) errors.Remove(propertyName);
RaiseErrorsChanged(propertyName);
}
}
public void RaiseErrorsChanged(string propertyName)
{
if (ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
#region INotifyDataErrorInfo Members
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public System.Collections.IEnumerable GetErrors(string propertyName)
{
if (String.IsNullOrEmpty(propertyName) ||
!errors.ContainsKey(propertyName)) return null;
return errors[propertyName];
}
public bool HasErrors
{
get => errors.Count > 0;
}
#endregion
}
View
TextBox.Text bindings must be set to TwoWay (which is the default Binding.Mode value for this property)
<DataGrid x:Name="gridUsers"
DataContext="{Binding Source={StaticResource viewModelUsers}}"
CanUserAddRows="False"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser"} />
<Grid DataContext="{Binding Source={StaticResource viewModelUsers}, Path=SelectedUser}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Row="0">UserName:</Label>
<TextBox x:Name="txtUsername" Grid.Row="0" Grid.Column="1"
Text="{Binding Username, NotifyOnValidationError=True"/>
<Label Grid.Row="1">Role:</Label>
<TextBox x:Name="txtRole" Grid.Row="1" Grid.Column="1"
Text="{Binding Role, NotifyOnValidationError=True}"/>
</Grid>
I am developing an application using c# and the Universal Windows Platform (UWP) and am struggling with creating a one-way data-bind between a layout control and an observable class. Currently, when the observable class property is changed, it does not update the UI element. I think it has something to do with the fact that I am binding a DataTemplate ListViewItem rather than a static layout element, but I am not sure if this is the problem or how to solve it. Any help would be appreciated. The code for the UI element and backend code is shown.
DataTemplate (XAML) (Styling is removed for readability)
<DataTemplate x:Key="variableTemplate"
x:DataType="local:VariableNode">
<Border>
<StackPanel Orientation="Vertical">
<Border>
<Grid>
<TextBlock Text="{Binding Name}" />
<StackPanel Orientation="Horizontal" >
<Button Tag="{Binding Description}"/>
<Button Tag="{Binding}"/>
</StackPanel>
</Grid>
</Border>
<Grid Margin="0, 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Border >
<Grid Grid.Column="0">
<Button Click="Choose_Measurement"
Tag="{Binding}">
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Bind Path=Measurement_Name, Mode=TwoWay}"
Foreground="{x:Bind MF}" />
<TextBlock Foreground="{x:Bind MF}" />
</StackPanel>
</Button>
</Grid>
</Border>
<Grid Grid.Column="1">
<Button Foreground="{Binding UF}"
Tag="{Binding}"
IsEnabled="{Binding Unit_Exists}"
Click="Choose_Unit">
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Bind Path=Unit_Name, Mode=OneWay}"
Foreground="{Binding UF}" />
<TextBlock Foreground="{Binding UF}" />
</StackPanel>
</Button>
</Grid>
</Grid>
</StackPanel>
</Border>
</DataTemplate>
C# Observable Class VariableNode (Irrelevant properties removed)
public class VariableNode : ExperimentNode
{
public VariableNode() { }
public VariableNode(VariableType type)
{
Type = type;
Name = name_ref[(int)Type];
Category = "Problem";
Unit = -1;
}
private string[] name_ref = { "Independent Variable", "Dependent Variable", "Controlled Variable" };
public enum VariableType { Independent, Dependent, Controlled };
public VariableType Type { get; set; }
public Measurement Measure { get; set; }
public int Unit { get; set; }
[XmlIgnoreAttribute]
public Measurement MeasureSource
{
get { return this.Measure; }
set
{
this.Measure = value;
OnPropertyChanged("Measurement_Name");
}
}
[XmlIgnoreAttribute]
public string Measurement_Name
{
get
{
if (Measure == null) { return "Select a Measurement"; }
else { return Measure.Name; }
}
set
{
if (Measure != null)
{
Measure.Name = value;
OnPropertyChanged();
}
}
}
[XmlIgnoreAttribute]
public string Unit_Name
{
get
{
if (Measure == null) { return "No measurement"; }
else if (Unit < 0) { return "Select a unit"; }
else { return Measure.Unit[Unit]; }
}
}
[XmlIgnoreAttribute]
public bool Unit_Exists
{
get { return Measure != null; }
}
}
C# XAML.CS code calling the property change
public void Choose_Measurement (object sender, RoutedEventArgs e)
{
Button butt = sender as Button
VariableNode sel = butt.Tag as VariableNode;
sel.Measurement_Name = "New Name";
}
Again thanks for the help, I know its a lot of code, and I appreciate the help in debugging / learning.
Ok, so I ended up finding the answer, and I think that it may help others trying to replicate what I am trying to do:
Basically, the class that one is trying to make observable must extend the class INotifyPropertyChanged. So, I ended up making a base class from which to extend all of my observable classes from:
public class BaseClass : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChanged(this, e);
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}