I have a problem for which I'm searching an explanation. It's similar to what's been discussed in WPF ComboBox SelectedItem Set to Null on TabControl Switch, but it's involving a lesser degree of binding and so should be open to simpler solutions. What I'm describing below is a simplified case I've built to reproduce and try to understand why the problem is arising.
So, the project is based on MVVM, and the main window has just a button labelled "Search", declared as follows:
<Button Margin="50,0,0,0" Width="150" Height="40" Content="Search" HorizontalAlignment="Left" Command="{Binding UpdateViewCommand}" CommandParameter="Search"/>
The code is bound to UpdateView :ICommand that, is defined as follows:
class UpdateViewCommand : ICommand
{
private MainViewModel viewModel;
public UpdateViewCommand(MainViewModel viewModel)
{
this.viewModel = viewModel;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
if (parameter.ToString() == "Search")
{
viewModel.SelectedViewModel = new SearchViewModel();
}
}
}
This view overlaps with the main one in the upper part, leaving the "Search" button visible, as shown in the picture below:
The view includes a ComboBox and a "Go" button, declared as:
<ComboBox Name="SearchCriterion" Canvas.Left="128" Canvas.Top="14" Height="22" Width="257" Background="#FF66CCFF" BorderBrush="Black" SelectedIndex="0"
SelectedItem="{Binding QueryType, Mode=OneWayToSource}">
<ComboBoxItem FontFamily="Calibri" FontSize="14" Background="#FF66CCFF">
Author
</ComboBoxItem>
<ComboBoxItem FontFamily="Calibri" FontSize="14" Background="#FF66CCFF">
Title
</ComboBoxItem>
</ComboBox>
<Button Name="SearchButton" Height="22" Content="Go" Canvas.Left="390" Canvas.Top="44" Width="45" BorderBrush="Black"
FontFamily="Calibri" FontSize="14" Background="#FF0066FF" Command="{Binding ExecQueryCmd}" Foreground="White"/>
All the button does is getting the ComboBoxItem value bound in the ComboBox declaration through the variable QueryType and print it. QueryType is declared as:
private ComboBoxItem _queryType = new ComboBoxItem();
public ComboBoxItem QueryType
{
get { return _queryType; }
set
{
Globals.mylog.Trace("In SearchViewModel.QueryType");
_queryType = value;
OnPropertyChanged(nameof(QueryType));
}
}
Assuming this is clear, here is the problem I see. I start the program, click on "Search" and the SearchView appears. I play with the ComboBox, click "Go" and all is fine. I can do this several times, no problem.
Now I click on "Search" again. No apparent change (the view is already there), but if I click on "Go" an exception is raised because the variable is null (I'm running under Visual Studio, so I can easily check). Note that if, instead of clicking "Go" right after clicking on "Search", I click on the ComboxBox and change its value before, everything works fine.
Can anyone explain me why this is happening, and how I can solve it?
Thanks
You never explicitly assigned a value to QueryType in the constructor of SearchViewModel, so the value in querytype was depending on the UI to update it.
A better way is to have the selectedvalue come from the viewmodel (and not have ui elements in tour viewmodels as I mentionned in the comments).
What I changed to make it works:
In SearchViewModel:
/// <summary>
/// Selected option to search by (it is now a string)
/// </summary>
private string _queryType;
public string QueryType
{
get { return _queryType; }
set
{
Globals.mylog.Trace("In SearchViewModel.QueryType");
_queryType = value;
OnPropertyChanged(nameof(QueryType));
}
}
/// <summary>
/// List of options to search by
/// </summary>
public ObservableCollection<string> Queries { get; set; }
public SearchViewModel()
{
Globals.mylog.Trace("In SearchViewModel");
//Initialization ofthe list of options
Queries = new ObservableCollection<string> { "Author", "Title" };
//Initialization of the selected item
this.QueryType = Queries.FirstOrDefault();
ExecQueryCmd = new RelayCommand(ExecuteQuery, CanExecuteQuery);
}
In SearchView:
<--The combobox is now bound to the list in the ViewModel(the data is stored in the viewmodels and the view is only responsible for displaying it) -->
<Canvas Width="517" Height="580" Background="#FFCCFF99">
<ComboBox Name="SearchCriterion" Canvas.Left="128" Canvas.Top="14" Height="22" Width="257" ItemsSource="{Binding Queries}" Background="#FF66CCFF" BorderBrush="Black"
SelectedItem="{Binding QueryType, Mode=TwoWay}">
<ComboBox.ItemContainerStyle>
<Style BasedOn="{StaticResource {x:Type ComboBoxItem}}" TargetType="{x:Type ComboBoxItem}">
<Setter Property="FontFamily" Value="Calibri"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Background" Value="#FF66CCFF"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
<Button Name="SearchButton" Height="22" Content="Go" Canvas.Left="390" Canvas.Top="44" Width="45" BorderBrush="Black"
FontFamily="Calibri" FontSize="14" Background="#FF0066FF" Command="{Binding ExecQueryCmd}" Foreground="White"/>
</Canvas>
Related
Im building a WPF application and trying to stick to the MVVM pattern as much as possible. I have a list box with a data template inside of it that contains a TextBlock and Button. If the button within the data template is clicked it does not select the entire row, so I am unaware of what row it pertains to. I would like to grab the entire object and bind it to a property in the view model. Can I get some help or a workaround for this please that sticks to mvvm pattern.
List box with item template
<telerik:RadListBox Width="200" Height="150" HorizontalAlignment="Left" Margin="10" ItemsSource="{Binding ListOfSupplierInvoices}"
SelectedItem="{Binding SelectedSupplierInvoice, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<telerik:RadListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" >
<TextBlock Text="{Binding InvoiceNumber}" HorizontalAlignment="Left" Margin="5" ></TextBlock>
<telerik:RadButton Height="20" >
<telerik:RadButton.Content>
<Image Source="/ImageResources/Misc/delete.png" Stretch="Fill" />
</telerik:RadButton.Content>
</telerik:RadButton>
</StackPanel>
</DataTemplate>
</telerik:RadListBox.ItemTemplate>
</telerik:RadListBox>
How it looks in the view:
As far as I understand your code, the button corresponds to a delete command, which means you want to delete the item associated with the button. In this case, the selection might not need to change, you just have to pass the current item to the delete command.
Add a Delete command to your view model like this:
public class MyViewModel : ViewModelBase
{
public MyViewModel()
{
Delete = new DelegateCommand(ExecuteDelete, CanExecuteDelete);
// ...other code.
}
public ICommand Delete { get; }
private void ExecuteDelete(object obj)
{
var invoiceItem = (InvoiceItem) obj;
// Use this only if you need the item to be selected.
// SelectedSupplierInvoice = invoiceItem;
// ...your delete logic.
}
private bool CanExecuteDelete(object obj)
{
// ...your can execute delete logic.
}
// ...other code.
}
Note that I introduced InvoiceItem as item type, because I do not know your item type, simply adapt it. The Delete command gets the current item passed as parameter. If you can always remove the item, there is no need in selecting it, as it is gone afterwards.
Otherwise, uncomment the line so the SelectedSupplierInvoice is set to the item which will automatically update the user interface through the two-way binding if you have implemented INotifyPropertyChanged correctly or derive from ViewModelBase which exposes the RaisePropertyChanged or OnPropertyChanged method, e.g.:
private InvoiceItem _selectedSupplierInvoice;
public InvoiceItem SelectedSupplierInvoice
{
get => _selectedSupplierInvoice;
set
{
if (_selectedSupplierInvoice == value)
return;
_selectedSupplierInvoice = value;
RaisePropertyChanged();
}
}
In your XAML wire the button to the Delete command on the DataContext of the RadListBox.
<telerik:RadButton Height="20"
Command="{Binding DataContext.Delete, RelativeSource={RelativeSource AncestorType={x:Type telerik:RadListBox}}}"
CommandParameter="{Binding}">
<telerik:RadButton.Content>
<Image Source="/ImageResources/Misc/delete.png" Stretch="Fill" />
</telerik:RadButton.Content>
</telerik:RadButton>
I have a button that displays the value from a class that I created. Everything works fine, except for the fact that the button content does not refresh once the value of the binding is changed in the code. If I exit the screen and come back, the value is correct. Staying on the same screen does not refresh the button content.
The button code is shown below.
<Grid x:Name="Task1Grid" Grid.Row="0" Grid.Column="0" Margin="5,0,5,0">
<Grid.RowDefinitions>
<RowDefinition Height=".2*"/>
<RowDefinition Height=".6*"/>
<RowDefinition Height=".2*"/>
</Grid.RowDefinitions>
<Button Grid.Row="1" Style="{StaticResource RoundedButtonStyle}" Tag="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Click="StoplightButton_Click" FontFamily="Global User Interface">
<Button.Content>
<Image Stretch="Uniform" Source="{Binding SelectedRepairOrder.TaskStatusGrid[0], Converter={StaticResource TaskStatusToStopLight}, Mode=OneWay}"/>
</Button.Content>
<Button.Background>
<ImageBrush Stretch="Uniform" ImageSource="{Binding SelectedRepairOrder.TaskStatusGrid[0], Converter={StaticResource TaskStatusToStopLight}, Mode=OneWay}"/>
</Button.Background>
</Button>
<Button x:Name="Task0Time" Tag="0" Style="{StaticResource RoundedButtonStyle}" Visibility="{Binding SelectedRepairOrder.TaskStatusGrid[0].NewTaskstatus, Converter=
{StaticResource TaskStatusToVisibility}}" IsEnabled="{Binding ShowForecastFeatures}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="{Binding SelectedRepairOrder.TaskStatusGrid[0].TmTimecmpltask, Converter={StaticResource TaskCompleteTimeToTime}}" Grid.Row="2" Flyout="{StaticResource Task1Flyout}"/>
<TextBlock Grid.Row="0" Text="{Binding ClientInfo.TasksInfo[0].TaskDescription}" TextAlignment="Center" VerticalAlignment="Bottom" FontSize="28"/>
</Grid>
The flyout code is shown below.
<Border x:Name="StopLightBorder" Background="CornflowerBlue" Grid.Row="1" BorderBrush="White" BorderThickness="2">
<Grid x:Name="StopLightGrid" Margin="5" >
<Grid.Resources>
<converter:TaskStatusToStopLight x:Key="TaskStatusToStopLight"/>
<converter:TaskCompleteTimeToTime x:Key="TaskCompleteTimeToTime"/>
<converter:TaskStatusToVisibility x:Key="TaskStatusToVisibility"/>
<Flyout x:Key="Task1Flyout" >
<ListBox ItemsSource="{Binding ForecastTimes}" Tag="0" SelectionChanged="ForecastTimeChanged"/>
</Flyout>
The code which changes the value for the binding is shown below.
private void ForecastTimeChanged(object sender, SelectionChangedEventArgs e)
{
var timeListBox = (ListBox)sender;
var completeTime = Convert.ToDateTime(e.AddedItems[0].ToString());
var taskNum = Convert.ToInt16(((FrameworkElement)sender).Tag);
var result = checkPreviousTaskTimes(completeTime, taskNum);
switch (result)
{
case ForecastResult.ValidTime:
globalContext.SelectedRepairOrder.TaskStatusGrid[taskNum].TmTimecmpltask = completeTime.ToString();
globalContext.SelectedRepairOrder.TaskStatusGrid[taskNum].DtDateoverride = completeTime.ToString();
globalContext.SelectedRepairOrder.TaskStatusGrid[taskNum].TmTimeoverride = completeTime.ToString();
globalContext.SelectedRepairOrder.TaskStatusGrid[taskNum].SendOverrideForecastTime = true;
globalContext.SelectedRepairOrder.WasChanged = true;
globalContext.SelectedRepairOrder.RecordGrid = "1";
((Popup)((FlyoutPresenter)((FrameworkElement)sender).Parent).Parent).IsOpen = false;
break;
default:
showForecastError(result, completeTime, taskNum);
break;
}
}
The Visibility and IsEnabled both work just fine. Not sure what else I can do at this point. It seems that changing the bound data does not have an effect until you leave the screen. I chased this issue all the way through and saw the changes to the data as well as everything else I expected. The flyout causes the forecasttimechanged method to activate. When we go to save this data to the database, the data is correct. The flyout shows the selected time when viewing it on the screen, which is what I want. I see that highlighted in the flyout.
If there is a better control to use than the button, I am all ears at this point. Here is the tricky part. This forecast time can be set in the application as well as the app you are seeing code from. The app has time in 15 minute increments, but the other program that can update this control can put in any time it wishes.
I know there is some control or parameter that needs to be set in order to make this happen properly, but for the life of me, I cannot find it. I have tried everything for the past 3 days now and nothing works.
Help me please.
I know there is some control or parameter that needs to be set in order to make this happen properly, but for the life of me, I cannot find it. I have tried everything for the past 3 days now and nothing works.
From your code, I guess the problem is that you have not implemented INotifyPropertyChanged for binding property. And your logic is complex, you could realize your feature with the easy way like the follow example.
<Button Content="{Binding SelectItem,Mode=OneWay}">
<Button.Flyout>
<Flyout Placement="Top">
<ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectItem,Mode=TwoWay}">
</ListBox>
</Flyout>
</Button.Flyout>
</Button>
Bind the button content with SelectItem, And then the button content will be modified automatically if the ListBox SelectedItem changed.
public class MainPageViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public List<string> Items { get; set; } = new List<string>();
private string selectItem = "Nico";
public string SelectItem { get { return selectItem; } set { selectItem = value; OnPropertyChanged(); } }
public MainPageViewModel()
{
Items.Add("Nico");
Items.Add("Song");
Items.Add("Xiao");
}
In my WPF application, I have a ListBox in my main screen. I'm trying to use the MVVM pattern, so I have a ViewModel associated with the View. When I launch the application, my ViewModel gets initiated, and it reads in a bunch of DLLs I've placed in a directory. Each DLL contains a "Strategy" class, so when I read the DLLs, I retrieve these Strategy class objects and put them in a list (actually an ObservableCollection) which is a member of my ViewModel. I'm using this member list, named DllList, to populate the ListBox.
My ViewModel looks like the following (unnecessary bits removed for clarity):
public class ViewModelPDMain : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName) {
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public ViewModelPDMain() {
dllList = new ObservableCollection<Strategy>();
selectedStrategy = new Strategy();
}
private ObservableCollection<Strategy> dllList = null;
private Strategy selectedStrategy = null;
public ObservableCollection<Strategy> DllList
{
get { return dllList; }
set {
dllList = value;
RaisePropertyChanged("DllList");
}
}
public Strategy SelectedStrategy
{
get { return selectedStrategy; }
set {
selectedStrategy = value;
RaisePropertyChanged("SelectedStrategy");
}
}
}
Then in my main View, I bind it as follows.
<Window x:Class="PrisonersDilemma.Source.View.ViewPDMain"
xmlns:local="clr-namespace:PrisonersDilemma.Source.View"
DataContext="{Binding Source={StaticResource mainViewModelLocator}, Path=ViewModelPDMain}"
Title="Iterated Prisoner's Dilemma" Height="500" Width="800" MinHeight="500" MinWidth="800">
<Grid Name="gridMain">
...
<!-- More stuff here -->
...
<ListBox Name="listStrategies" SelectedIndex="0"
ItemsSource="{Binding DllList}" SelectedItem="{Binding SelectedStrategy}"
Grid.Column="0" Grid.Row="1" Grid.RowSpan="2"
Width="Auto" MinWidth="120"
Margin="3"
BorderBrush="LightGray" BorderThickness="1">
</ListBox>
...
<!-- More stuff here -->
...
</Grid>
</Window>
When I do this and run the application my list box looks like below which is expected.
The problem is when I try to display a property inside my Strategy objects. My Strategy class contains another class, named StratInfo, which in turn contains a string property, StrategyName. My requirement is to display this string value as listbox item values instead of what you can see above.
So I do the following in my View:
<Window x:Class="PrisonersDilemma.Source.View.ViewPDMain"
xmlns:local="clr-namespace:PrisonersDilemma.Source.View"
DataContext="{Binding Source={StaticResource mainViewModelLocator}, Path=ViewModelPDMain}"
Title="Iterated Prisoner's Dilemma" Height="500" Width="800" MinHeight="500" MinWidth="800">
<Grid Name="gridMain">
...
<!-- More Stuff Here -->
...
<ListBox Name="listStrategies" SelectedIndex="0"
ItemsSource="{Binding DllList}" SelectedItem="{Binding SelectedStrategy}"
Grid.Column="0" Grid.Row="1" Grid.RowSpan="2"
Width="Auto" MinWidth="120"
Margin="3"
BorderBrush="LightGray" BorderThickness="1">
<!-- Added Stuff -->
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Label Name="lblFirstName"
Content="{Binding SelectedStrategy.StratInfo.StrategyName, Mode=OneWay}"
Grid.Column="0"></Label>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
...
<!-- More Stuff Here -->
...
</Grid>
</Window>
When I do this, I expect the list box items to contain a label, and it to display my StrategyName value. However, I get a listbox which contains 25 items (I have 25 DLLs), but all 25 items are empty.
Funny thing is, I tried to bind the SelectedStrategy.StratInfo.StrategyName to a text box Text property, and it worked. That is, when I click any empty listbox item, it displays the StrategyName in the text box. Please refer to the following figure. You can see that the listbox contains items but the content values aren't displayed. In addition, to the right, the Strategy Name text box is a text box where I have bound the SelectedStrategy.StratInfo.StrategyName and it displays the correct value on item select event.
I have done this exact same thing in a simpler project, and it works just fine. I can't figure out what I'm doing wrong here.
Any thoughts?
Your binding in the data template is incorrect. The data context within the data template is an item in the DllList which is of type Strategy. So your Label should be like so:
<Label Name="lblFirstName"
Content="{Binding StratInfo.StrategyName, Mode=OneWay}"
Grid.Column="0"/>
I have a WPF/MVVM project in C #/FrameWork 4.0
In my view I have two ControlBox "NoRSAC" and "LieuRSAC"
<View:StateControlTextBox
x:Name="NoRSAC"
ReadOnly="{Binding IsReadOnly}"
ViewModelDataType="UtilisateurSaisieViewModel"
TableDataType="TUtilisateurDataTable"
Tag="{DynamicResource TELEPHONE}"
Text="{Binding UserVM.No_RSAC, Mode=TwoWay}" Margin="0" Canvas.Top="140" Width="185" VerticalAlignment="Stretch" />
<View:StateControlTextBox
x:Name="LieuRSAC"
ReadOnly="{Binding IsReadOnly}"
ViewModelDataType="UtilisateurSaisieViewModel"
TableDataType="TUtilisateurDataTable"
Tag="{DynamicResource TELEPHONE}"
Text="{Binding UserVM.Lieu_RSAC, Mode=TwoWay}" Margin="0" Canvas.Top="140" Width="185" VerticalAlignment="Stretch"/>
</Canvas>
And ControlComboBox "cmbFonction"
<View:StateControlComboBox
x:Name="cmbFonction"
ReadOnlyControlState="Disabled"
IsReadOnly="{Binding IsReadOnly}"
ViewModelDataType="UtilisateurSaisieViewModel"
TableDataType="TUtilisateurDataTable"
ItemsSource="{Binding ListeFonctions}"
SelectedValue="{Binding UserVM.Fonction, Mode=TwoWay}" Width="303" Margin="0" HorizontalAlignment="Left" Canvas.Left="97" Canvas.Top="108" />
I want to view the ControlBox "NoRSAC" and "LieuRSAC" when I select a particular valeure in the ComboBox "cmbFonction" and hide when it's another selected value
Thank you for your help
In the set method of the property Fonction, you can check the value and update another property that you should introduce in your view model and that is of type System.Windows.Visibility. In the following example, I call this property TextBoxVisibility:
public class UserVM : INotifyPropertyChanged
{
private Visibility _textBoxVisibility;
public Visibility TextBoxVisibility
{
get { return _textBoxVisibility; }
set
{
_textBoxVisibility = value;
OnPropertyChanged();
}
}
public string Fonction
{
get { return _fonction; }
set
{
_fonction = value;
OnPropertyChanged();
if (value == "Value A")
TextBoxVisibility = Visibility.Hidden;
else
TextBoxVisibility = Visibility.Visible;
}
}
// Other members omitted for sake of simplicity.
}
Please note that you need to implement INotifyPropertyChanged (directly or indirectly) so that the changes of the property values are forwarded to the bindings that can in turn update the dependency properties of the controls in your view.
Thus you must not forget to add an additional binding to all of your text boxes in your view. Here is an example for that, the important part is the binding on Visibility:
<View:StateControlTextBox
x:Name="NoRSAC"
ReadOnly="{Binding IsReadOnly}"
ViewModelDataType="UtilisateurSaisieViewModel"
TableDataType="TUtilisateurDataTable"
Tag="{DynamicResource TELEPHONE}"
Visibility="{Binding UserVM.TextBoxVisibility}"
Text="{Binding UserVM.No_RSAC, Mode=TwoWay}" Margin="0" Canvas.Top="140" Width="185" VerticalAlignment="Stretch" />
I have two WPF windows developed using the surface SDK, one that is a data entry form, and the second dispays the data in a listbox. The listbox displays the data perfectly but when I add a new record using the data entry form, the listbox is not updated until I reopen the window. Is there a way to automatically update the listbox through binding or something?
This is the listbox code:
<s:SurfaceListBox Height="673" Margin="0,26,0,31" Name="surfaceListBox1" ItemsSource="{Binding Path={}}" Width="490">
<s:SurfaceListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label Width="80" FontSize="8" Content="{Binding Path=item1}"></Label>
<Label Width="80" FontSize="8" Content="{Binding Path=item2}"></Label>
<Label Width="210" FontSize="8" Content="{Binding Path=item3}"></Label>
<Label Width="80" FontSize="8" Content="{Binding Path=item4}"></Label>
<Label Width="60" FontSize="8" Content="{Binding Path=item5, Converter={StaticResource booleanconverter}}"></Label>
</StackPanel>
</DataTemplate>
</s:SurfaceListBox.ItemTemplate>
</s:SurfaceListBox>
I am using Visual C# 2008 and the code to fill the listbox is:
private SHIPS_LOGDataSet ShipData = new SHIPS_LOGDataSet();
private SHIPS_LOGDataSetTableAdapters.MAINTableAdapter taMain = new SHIPS_LOGDataSetTableAdapters.MAINTableAdapter();
private SHIPS_LOGDataSetTableAdapters.TableAdapterManager taManager = new ShipsLogSurface.SHIPS_LOGDataSetTableAdapters.TableAdapterManager();
private void SurfaceWindow_Loaded(object sender, RoutedEventArgs e)
{
this.taMain.Fill(this.ShipData.MAIN);
this.DataContext = from MAIN in this.ShipData.MAIN orderby MAIN.MESSAGE_ID descending select MAIN;
}
The only table in my database is called MAIN.
I'm guessing I might have to use a collection view or similar but don't know how to implement that. Any ideas would be much appreciated. Thanks
INotifyPropertyChanged is an interface which you should implement in your data class (ShipData?). The properties in your data class should look as follows:
private string _myField;
public string MyField {
get { return _myField; }
set { _myField = value; onPropertyChanged(this, "MyField"); }
}
So whenever something in your data class changes (i.e. add/delete/update), it will fire the OnPropertyChanged event.
Your List or ObservableCollection that you use to populate the list listens to this OnPropertyChanged event and will update itself whenever the event is fired.
Try to do it with INotifyPropertyChanged.
surfaceListBox1.Items.Refresh();