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);
}
}
}
Related
I can add texts to the DataGrid and by clicking the button (edit) I can drag the data from the DataGrid back into the text boxes, but my problem is if I change the data after dragging it over, it is not updated in the Datagrid.
<Canvas x:Name="CV_Projekte" Grid.Column="1" Background="White" Visibility="Visible">
<Label Content="Projektnumber:" Canvas.Left="44" Canvas.Top="84" FontSize="14" FontWeight="Bold"/>
<Label Content="Name:" Canvas.Left="43" Canvas.Top="113" FontSize="14" FontWeight="Bold" />
<Label Content="Unterposition:" Canvas.Left="44" Canvas.Top="146" FontSize="14" FontWeight="Bold" />
<Label Content="Describe:" Canvas.Left="44" Canvas.Top="182" FontSize="14" FontWeight="Bold" />
<TextBox Canvas.Left="183" Canvas.Top="89" TextWrapping="Wrap" Width="120" Text="{Binding ProjectNumber}" />
<TextBox Canvas.Left="44" Canvas.Top="216" TextWrapping="Wrap" Width="273" Height="155"/>
<TextBox Canvas.Left="183" Canvas.Top="151" TextWrapping="Wrap" Width="120"/>
<TextBox Canvas.Left="183" Canvas.Top="118" TextWrapping="Wrap" Width="120" Text="{Binding ProjectName}"/>
<Button Content="Add" Canvas.Left="383" Canvas.Top="167" Width="75" Command="{Binding StartCommand}" Opacity="0.2"/>
<Button Content="Edit" Canvas.Left="383" Canvas.Top="201" Width="75" Opacity="0.2" Command="{Binding UpdateCommand}" />
<DataGrid Height="575" Canvas.Left="616" Width="303" ItemsSource="{Binding CareList}" SelectedItem="{Binding SelectedCare}" >
<DataGrid.Columns>
<DataGridTextColumn Header="Number" Binding="{Binding Projektnummer}"/>
<DataGridTextColumn Header="Projektname" Width="184" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Delete"/>
</DataGrid.Columns>
</DataGrid>
</Canvas>
This is the CareViewModel.cs
public class CareViewModel :INotifyPropertyChanged
{
private int _ProtjectNumber;
private string _ProjectName;
private Visibility _TrackingVisibility;
private Visibility _CarreVisibility;
private Care _selectedCare;
public int ProjectNumber
{
get
{
return _ProtjectNumber;
}
set
{
_ProtjectNumber = value;
OnPropertyChanged(nameof(ProjectNumber));
}
}
public string ProjectName
{
get
{
return _ProjectName;
}
set
{
_ProjectName = value;
OnPropertyChanged(nameof(ProjectName));
}
}
public Visibility TrackingVisibility
{
get
{
return _TrackingVisibility;
}
set
{
_TrackingVisibility = value;
OnPropertyChanged(nameof(_TrackingVisibility));
}
}
public Visibility CareVisibility
{
get
{
return _CarreVisibility;
}
set
{
_CarreVisibility = value;
OnPropertyChanged(nameof(CareVisibility));
}
}
public void TrackingView(RoutedEventArgs e)
{
TrackingVisibility = Visibility.Visible;
CareVisibility = Visibility.Collapsed;
}
public void CareView(RoutedEventArgs e)
{
TrackingVisibility = Visibility.Collapsed;
CareVisibility = Visibility.Visible;
}
public ObservableCollection<Care> CareList { get; set; }
public CareViewModel()
{
CareList = new ObservableCollection<Care>(); //List
StartCommand = new OurCommand<RoutedEventArgs>(Setting);
TrackingCommand = new OurCommand<RoutedEventArgs>(TrackingView);
CareCommand = new OurCommand<RoutedEventArgs>(CareView);
UpdateCommand = new OurCommand<RoutedEventArgs>(Bearbeitung);
TrackingVisibility = Visibility.Visible;
CareVisibility = Visibility.Collapsed;
}
public ICommand StartCommand { get; set; }
public ICommand TrackingCommand { get; set; }
public ICommand CareCommand { get; set; }
public ICommand UpdateCommand { get; set; }
public Care SelectedCare
{
get
{
return _selectedCare;
}
set
{
_selectedCare = value;
OnPropertyChanged(nameof(SelectedCare));
}
}
public void Setting(RoutedEventArgs e) // Add the Values to Datagrid
{
var projects = new Care();
projects.Projektnummer = _ProtjectNumber;
projects.Name = _ProjectName;
ProjectNumber = 0;
ProjectName = null;
SelectedCare = projects;
CareList.Add(projects);
}
public void Bearbeitung(RoutedEventArgs e) //Editing the value button
{
ProjectNumber = SelectedCare.Projektnummer;
ProjectName = SelectedCare.Name;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Class Care
public class Care
{
private int _Projektnummer;
private string _Name;
public int Projektnummer
{
get { return _Projektnummer;}
set { _Projektnummer = value;}
}
public string Name
{
get { return _Name;}
set { _Name = value;}
}
}
Thanks to everyone who wants to help me.
I've created some demo project from source you provided.
Selection of the row fills TextBoxes automatically
Add Button creates new row and make it able to edit
Save button applies changes in TextBoxes
X button deletes the row
0. MVVM Helpers
I don't know what is OurCommand but I'm using almost unchanged this one.
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly 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)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
}
It supports command parameters and everything else related to ICommand.
To be able not to implement INotifyPropertyChanged in every View Model, I moved the implementation to the base class.
public class NotifyPropertyChanged : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
[CallerMemberName] is a compiler trick that allows to call OnPropertyChanged() without arguments. Compiler will fill it automatically with Caller Member Name :) - Property name.
1. Markup
Use Grid and relative positioning of controls. It will help you to make responsive design where layout looks fine for almost any area size.
<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Width="1200" Height="600">
<Window.DataContext>
<local:CareViewModel/>
</Window.DataContext>
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.5*"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="TextBlock">
<Setter Property="MinHeight" Value="20"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="MinHeight" Value="20"/>
<Setter Property="Margin" Value="5"/>
<Style.Triggers>
<DataTrigger Binding="{Binding EditingCare}" Value="{x:Null}">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="Button">
<Setter Property="Width" Value="75"/>
<Setter Property="Margin" Value="5"/>
</Style>
</Grid.Resources>
<StackPanel>
<TextBlock Text="Projektnumber:"/>
<TextBlock Text="Name:"/>
<TextBlock Text="Unterposition:"/>
<TextBlock Text="Describe:"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBox Text="{Binding EditingCare.ProjectNumber}"/>
<TextBox Text="{Binding EditingCare.Name}"/>
<TextBox Text="{Binding EditingCare.Subtitle}"/>
</StackPanel>
<TextBox TextWrapping="Wrap" Grid.Row="1" Grid.ColumnSpan="2" Text="{Binding EditingCare.Description}"/>
<DockPanel Margin="5" HorizontalAlignment="Right" Grid.Row="2" Grid.ColumnSpan="2">
<Button Content="Add" Command="{Binding AddCommand}" />
<Button Content="Save" Command="{Binding UpdateCommand}" />
</DockPanel>
</Grid>
<DataGrid Grid.Column="1" ItemsSource="{Binding CareList}" SelectedItem="{Binding SelectedCare}" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Number" Binding="{Binding ProjectNumber}"/>
<DataGridTextColumn Header="Projektname" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Unterposition" Binding="{Binding Subtitle}" Width="*"/>
<DataGridTemplateColumn Header="Delete">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="X" Command="{Binding DataContext.DeleteCommand,RelativeSource={RelativeSource AncestorType=DataGrid}}" CommandParameter="{Binding}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
2. Data
To fix the initial problem I've implemented INotifyPropertyChanged for the Care class.
public class Care : NotifyPropertyChanged
{
private int _projectNumber;
private string _name;
private string _description;
private string _subtitle;
public int ProjectNumber
{
get => _projectNumber;
set
{
_projectNumber = value;
OnPropertyChanged();
}
}
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
public string Subtitle
{
get => _subtitle;
set
{
_subtitle = value;
OnPropertyChanged();
}
}
public string Description
{
get => _description;
set
{
_description = value;
OnPropertyChanged();
}
}
public Care() { }
public Care(Care care)
{
ProjectNumber = care.ProjectNumber;
Name = care.Name;
Subtitle = care.Subtitle;
Description = care.Description;
}
}
2. View Model
I played a lot with code and came up with following one.
public class CareViewModel : NotifyPropertyChanged
{
private bool _trackingVisible;
private bool _careVisible;
private Care _selectedCare;
private Care _editingCare;
private ObservableCollection<Care> _careList;
private ICommand _addCommand;
private ICommand _trackingCommand;
private ICommand _careCommand;
private ICommand _updateCommand;
private ICommand _deleteCommand;
public ObservableCollection<Care> CareList
{
get => _careList;
set
{
_careList = value;
OnPropertyChanged();
}
}
public bool TrackingVisibility
{
get => _trackingVisible;
set
{
_trackingVisible = value;
OnPropertyChanged();
}
}
public bool CareVisibility
{
get => _careVisible;
set
{
_careVisible = value;
OnPropertyChanged();
}
}
public Care SelectedCare
{
get => _selectedCare;
set
{
_selectedCare = value;
EditingCare = value is Care care ? new Care(care) : value;
OnPropertyChanged();
}
}
public Care EditingCare
{
get => _editingCare;
set
{
_editingCare = value;
OnPropertyChanged();
}
}
public ICommand TrackingCommand => _trackingCommand ?? (_trackingCommand = new RelayCommand(parameter =>
{
TrackingVisibility = true;
CareVisibility = false;
}));
public ICommand CareCommand => _careCommand ?? (_careCommand = new RelayCommand(parameter =>
{
TrackingVisibility = false;
CareVisibility = true;
}));
// Add the Values to Datagrid
public ICommand AddCommand => _addCommand ?? (_addCommand = new RelayCommand(parameter =>
{
Care newItem = new Care();
CareList.Add(newItem);
SelectedCare = newItem;
}));
//Editing the value button
public ICommand UpdateCommand => _updateCommand ?? (_updateCommand = new RelayCommand(parameter =>
{
int index = CareList.IndexOf(SelectedCare);
CareList[index] = EditingCare;
SelectedCare = CareList[index];
}, parameter => EditingCare != null && SelectedCare != null));
public ICommand DeleteCommand => _deleteCommand ?? (_deleteCommand = new RelayCommand(parameter =>
{
if (parameter is Care care)
{
CareList.Remove(care);
}
}));
public CareViewModel()
{
CareList = new ObservableCollection<Care>();
TrackingVisibility = true;
CareVisibility = false;
}
}
3. Visibility
Among other changes pay attention to former Visibility properties now they are bool. And to attach it to controls here's converter that is already meintoined in Markup.
MVVM doesn't suggest to use UI-related types in View Model or Model unless you need it for special purpose.
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is bool v && v ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> null;
}
Usage of converter
<Button Visibility="{Binding TrackingVisibility, Converter={StaticResource BoolToVisibilityConverter}}"/>
I am using Material Design in XAML Toolkit. I have the main window with drawer, which contains the list of user controls (app tabs). When I click on them - application tab switches between this controls. I want to add a button to the window, and when I click on it I want to switch between tabs too. You can see important parts of my code here:
<materialDesign:DialogHost Identifier="RootDialog" SnackbarMessageQueue="{Binding ElementName=MainSnackbar, Path=MessageQueue}">
<materialDesign:DrawerHost IsLeftDrawerOpen="{Binding ElementName=MenuToggleButton, Path=IsChecked}">
<materialDesign:DrawerHost.LeftDrawerContent>
<DockPanel MinWidth="212">
<ToggleButton Style="{StaticResource MaterialDesignHamburgerToggleButton}"
DockPanel.Dock="Top"
HorizontalAlignment="Right" Margin="16"
IsChecked="{Binding ElementName=MenuToggleButton, Path=IsChecked, Mode=TwoWay}" />
<ListBox x:Name="DemoItemsListBox" Margin="0 16 0 16" SelectedIndex="0"
ItemsSource="{Binding DemoItems}"
PreviewMouseLeftButtonUp="UIElement_OnPreviewMouseLeftButtonUp">
<ListBox.ItemTemplate>
<DataTemplate DataType="helpers:DemoItem">
<TextBlock Text="{Binding Name}" Margin="32 0 32 0" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</materialDesign:DrawerHost.LeftDrawerContent>
<DockPanel>
<materialDesign:ColorZone Padding="16" materialDesign:ShadowAssist.ShadowDepth="Depth2"
Mode="PrimaryDark" DockPanel.Dock="Top">
<DockPanel>
<ToggleButton Style="{StaticResource MaterialDesignHamburgerToggleButton}" IsChecked="False"
x:Name="MenuToggleButton"/>
<materialDesign:PopupBox x:Name="popupBox">
<TextBlock>Check me please</TextBlock>
</materialDesign:PopupBox>
<CheckBox x:Name="LizenzeCheckBox" DockPanel.Dock="Right" Style="{StaticResource MaterialDesignCheckBox}" Tag="False">
<CheckBox.IsChecked>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}">
<Binding.ValidationRules>
<helpers:IsCheckedValidationRule />
</Binding.ValidationRules>
</Binding>
</CheckBox.IsChecked>CheckBox text</CheckBox>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="22">My App</TextBlock>
</DockPanel>
</materialDesign:ColorZone>
<Button x:Name="TheBUTTON" Click="Button_Click">Ckicc</Button>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ScrollViewer Grid.Row="1"
HorizontalScrollBarVisibility="{Binding ElementName=DemoItemsListBox, Path=SelectedItem.HorizontalScrollBarVisibilityRequirement}"
VerticalScrollBarVisibility="{Binding ElementName=DemoItemsListBox, Path=SelectedItem.VerticalScrollBarVisibilityRequirement}"
Padding="{Binding ElementName=DemoItemsListBox, Path=SelectedItem.MarginRequirement}">
<ContentControl Content="{Binding ElementName=DemoItemsListBox, Path=SelectedItem.Content}" />
</ScrollViewer></Grid>
This is my main window xaml code, as you can see, I bind ListBox Values to DemoItem[] array from viewModel. "TheButton" onclick event is the event which I want to use for tab switching.
My main window view model is:
public class MainWindowViewModel
{
public DemoItem[] DemoItems { get; }
public MainWindowViewModel(ISnackbarMessageQueue snackbarMessageQueue)
{
if (snackbarMessageQueue == null) throw new ArgumentNullException(nameof(snackbarMessageQueue));
DemoItems = new[]
{
new DemoItem("Tab1", new Tab1()),
new DemoItem("Tab2", new Tab2()),
new DemoItem("Tab3", new Tab3()),
};
}
}
The MainWindow.cs is:
public partial class MainWindow : Window
{
public static Snackbar Snackbar;
public MainWindow()
{
InitializeComponent();
Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
}).ContinueWith(t =>
{
MainSnackbar.MessageQueue.Enqueue("Welcome to my app");
}, TaskScheduler.FromCurrentSynchronizationContext());
DataContext = new MainWindowViewModel(MainSnackbar.MessageQueue);
}
private void UIElement_OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
//until we had a StaysOpen glag to Drawer, this will help with scroll bars
var dependencyObject = Mouse.Captured as DependencyObject;
while (dependencyObject != null)
{
if (dependencyObject is ScrollBar) return;
dependencyObject = VisualTreeHelper.GetParent(dependencyObject);
}
MenuToggleButton.IsChecked = false;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
//what to do here?
}
}
The DemoItem Class is:
public class DemoItem : INotifyPropertyChanged
{
private string _name;
private object _content;
private ScrollBarVisibility _horizontalScrollBarVisibilityRequirement;
private ScrollBarVisibility _verticalScrollBarVisibilityRequirement;
private Thickness _marginRequirement = new Thickness(16);
public DemoItem(string name, object content)
{
_name = name;
Content = content;
}
public string Name
{
get { return _name; }
set { this.MutateVerbose(ref _name, value, RaisePropertyChanged()); }
}
public object Content
{
get { return _content; }
set { this.MutateVerbose(ref _content, value, RaisePropertyChanged()); }
}
public ScrollBarVisibility HorizontalScrollBarVisibilityRequirement
{
get { return _horizontalScrollBarVisibilityRequirement; }
set { this.MutateVerbose(ref _horizontalScrollBarVisibilityRequirement, value, RaisePropertyChanged()); }
}
public ScrollBarVisibility VerticalScrollBarVisibilityRequirement
{
get { return _verticalScrollBarVisibilityRequirement; }
set { this.MutateVerbose(ref _verticalScrollBarVisibilityRequirement, value, RaisePropertyChanged()); }
}
public Thickness MarginRequirement
{
get { return _marginRequirement; }
set { this.MutateVerbose(ref _marginRequirement, value, RaisePropertyChanged()); }
}
public event PropertyChangedEventHandler PropertyChanged;
private Action<PropertyChangedEventArgs> RaisePropertyChanged()
{
return args => PropertyChanged?.Invoke(this, args);
}
}
My MutateVerbose function looks like:
public static void MutateVerbose<TField>(this INotifyPropertyChanged instance, ref TField field, TField newValue, Action<PropertyChangedEventArgs> raise, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<TField>.Default.Equals(field, newValue)) return;
field = newValue;
raise?.Invoke(new PropertyChangedEventArgs(propertyName));
}
I don't know how to switch tabs with button click in this situation. Help me, please!
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));
}
}
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.
I am trying to set the property of the MainViewModel from LoginViewModel using an instance of the MainViewModel inside LoginViewModel . But the binding in XAML does not get notified ! I set the break point inside the MainViewModel and it shows the value is being set, but if its set through instance it does not work. Can anyone help?
Below are the codes for both viewmodel. As you can see, in my loginviewmodel I set the public property using the instance of MainViewModel :
MainViewModel.Instance.Mainviewpageindex = 1;
MainViewModel
namespace DataRetrieval.ViewModels
{
public class MainViewModel : BindableBase
{
private static MainViewModel _instance = new MainViewModel();
public static MainViewModel Instance { get { return _instance; }
}
private int _mainviewpageindex;
public int Mainviewpageindex
{
get {
return _mainviewpageindex; }
set {
SetProperty(ref _mainviewpageindex, value);
// _mainviewpageindex = value;
// RaisePropertyChangedEvent("Mainviewpageindex");
}
}
public MainViewModel()
{
Mainviewpageindex = 0;
}
public DelegateCommand<object> _loginCommand;
public ICommand LoginCommand
{
get
{
_loginCommand = new DelegateCommand<object>(Login, VerifyLogin);
return _loginCommand;
}
}
private void Login(object context)
{
Mainviewpageindex = 1;
}
private bool VerifyLogin(object context)
{
return true;
}
}
LoginViewModel
namespace DataRetrieval.ViewModels
{
public class LoginViewModel : BindableBase
{
// public SecureString SecurePassword { private get; set; }
private string _uname;
public string Uname
{
get { return _uname; }
set {
SetProperty(ref _uname, value);
_loginCommand.RaiseCanExecuteChanged();
}
}
private SecureString _securePassword;
public SecureString SecurePassword
{
get { return _securePassword; }
set {
SetProperty(ref _securePassword, value);
_loginCommand.RaiseCanExecuteChanged();
}
}
public LoginViewModel()
{
}
public DelegateCommand<object> _loginCommand;
public ICommand LoginCommand
{
get
{
_loginCommand = new DelegateCommand<object>(Login, VerifyLogin);
return _loginCommand;
}
}
private void Login(object context)
{
if (SecurePassword != null || Uname != null)
{
//PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
// bool validated = ctx.ValidateCredentials(Uname, SecureStringToString(SecurePassword));
MainViewModel.Instance.Mainviewpageindex = 1;
}else
{
}
}
String SecureStringToString(SecureString value)
{
IntPtr valuePtr = IntPtr.Zero;
try
{
valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
return Marshal.PtrToStringUni(valuePtr);
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
}
}
private bool VerifyLogin(object context)
{
return true;
}
}
}
XAML:
<Window x:Class="DataRetrieval.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:DataRetrieval.Views"
xmlns:prism="http://www.codeplex.com/prism"
xmlns:viewModels="clr-namespace:DataRetrieval.ViewModels"
Title="Data Retrieval Tool" Height="Auto" Width="900" Icon="Resources/colorful_query_mark_light_bulb_28261523_y2o_icon.ico">
<Window.DataContext>
<viewModels:MainViewModel />
</Window.DataContext>
<TabControl Name="TabControl1" BorderBrush="{x:Null}" Background="White" SelectedIndex="{Binding Mainviewpageindex, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</TabControl.ItemContainerStyle>
<TabItem Header="General">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*" MaxWidth="300"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50"/>
<RowDefinition Height="auto" />
<RowDefinition Height="300"/>
</Grid.RowDefinitions>
<views:Login HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column="1" Grid.Row="1" />
<TextBlock TextWrapping="Wrap" TextAlignment="Center" Text="User your OneAbbott domain user name and password to login into the system" Grid.Column="1" Grid.Row="2"/>
<Button x:Name="btnLogin" Content="Login" Margin="20" Command="{Binding LoginCommand}"/>
</Grid>
</TabItem>
<TabItem Header="Second Tab">
<StackPanel>
<TextBlock TextWrapping="Wrap" TextAlignment="Center" Text="Login Successfull" Grid.Column="1" Grid.Row="2"/>
</StackPanel>
</TabItem>
</TabControl>
</Window>
You are dealing with two different instances of your MainViewModel. The one actually hooked up to your view is constructed by the XAML parser when it hits the code that is setting your data context.
The other you are creating as a separate static object inside your viewmodel and exposing through the Instance property.
You need a different way to communicate between the ViewModel instances. Pub-Sub events in Prism are the best way to deal with this kind of scenario, although using a CompositeCommand would also work.
It seems the best way to implement the communication between ModelViews is using prism Event Aggregation !
An example of it is provided here on MSDN: https://msdn.microsoft.com/en-us/library/ff921173(v=pandp.40).aspx