Winui3, Listview, inside flyout, bound to obsevablecollection, not updating - c#

I am currently displaying a ListView in a Flyout of a DropDownButton. I have an ObservableCollection that is bound to the ListView. During the app initialization, a method that clears the contents of the ObservableCollection and adds some elements to it, is called, this should update the ListView and show some elements on the UI, but an empty container is shown instead.
XAML:
<DropDownButton Content="Super long name that does not fit on the dropdown">
<DropDownButton.Flyout>
<Flyout Placement="Bottom" >
<Flyout.FlyoutPresenterStyle>
<Style TargetType="FlyoutPresenter">
<Setter Property="Padding" Value="3"/>
<Setter Property="CornerRadius" Value="5"/>
</Style>
</Flyout.FlyoutPresenterStyle>
<ListView
ItemsSource="{x:Bind _presenter.ViewModel.OutputDeviceViewModelList, Mode=OneWay}"
DisplayMemberPath="ListName"
SelectedValuePath="SelectedName"
CornerRadius="10"
SelectionMode="None"
IsItemClickEnabled="True"
ItemClick="OnMasterChannelDropdownSelectionChange"
/>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
Main Window (List Initialization call):
public MainWindow()
{
this.InitializeComponent();
_presenter = new Presenter(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
_controller = new Controller();
// Initialize states
_controller.GetDubwareDeviceInfo(); // Trigger a flow that ends up calling the UpdateOutputDevices on the Presenter
}
Presenter code:
public class Presenter
{
public Microsoft.UI.Dispatching.DispatcherQueue DispatcherQueue { get; }
public ViewModel ViewModel { get; } = new();
public void UpdateOutputDevices(List<OutputDeviceDs> outputDeviceDsList)
{
DispatcherQueue.TryEnqueue(() =>
{
ViewModel.OutputDeviceViewModelList.Clear();
ViewModel.OutputDeviceViewModelList.Add(new("", "SelectDevice", "None"));
foreach (OutputDeviceDs outputDeviceDs in outputDeviceDsList)
{
ViewModel.OutputDeviceViewModelList.Add(new(outputDeviceDs.Name, outputDeviceDs.Name, outputDeviceDs.Name));
}
Debug.WriteLine($" Updated output device list, outputDeviceDsList has {outputDeviceDsList.Count} elements and OutputDeviceViewModelList has {ViewModel.OutputDeviceViewModelList.Count} elements");
});
}
}
View model (contains the ObservableCollection):
public class ViewModel
{
public ViewModel() { }
public ObservableCollection<OutputDeviceViewModel> OutputDeviceViewModelList { get; set; } = new();
}
Model:
public class OutputDeviceViewModel
{
public string Id { get; set;}
public string SelectedName { get; set;}
public string ListName { get; set;}
public OutputDeviceViewModel(string id, string selectedName, string listName)
{
Id = id;
SelectedName = selectedName;
ListName = listName;
}
}
When the app is executed Debug print show that the list is populated with 3 elements
But an empty container is shown when the dropdown is open
Is only after interacting with other elements of the page that the list shows three elements.
Currently I have managed force the update of the ListView by creating a non visible ListView, that is bound to the same ObservableCollection, at the same level of the DropDownButton
as such:
<ListView
Visibility="Collapsed"
ItemsSource="{x:Bind _presenter.ViewModel.OutputDeviceViewModelList, Mode=OneWay}"
/>
<DropDownButton Content="Super long name that does not fit on the dropdown">
<DropDownButton.Flyout>
<Flyout Placement="Bottom" >
...
...
...
With this workarround the list shows the 3 elements as soon as the ObservableCollection is updated during intialization
Does anyone has any clue why this this workaround fixes the ListView elements not updating as soon as the list is udpated?

Related

Multiple rows in datagrid are changing when only the row explicitly changed should be

I have a two datagrids, both using identical xaml (other than one having an extra column). One is in the main window, the other is inside a usercontrol. Both are bound to a collection of the same items (one is currently an ObservableCollection, the other is an array). Project uses MVVM (or a close approximation).
The array/main window one works correctly - the Location combobox loads in the selected value (where it isn't null) and changing that value only affects the row it is changed in.
The ObservableCollection/usercontrol one does not. The values in the Location column do not load when the window opens, until you scroll down then back up again. Changing the Location combobox changes the value displayed in every visible row (or every row if virtualisation is turned off)... including in the disabled comboboxes. The same does not happen for the Bonding Level combobox, which is handled essentially identically. This happens whether the data presented is an array or an ObservableCollection.
Each instance of the class used to produce a row should be entirely separate, there's no codebehind to mess with values, and extremely similar xaml and c# in another file using the exact same type in the collection works. I am lost!
Screenshots are as follows:
Correct behaviour (Main window, array)
https://i.imgur.com/SJIsTOT.png (Loaded immediately, no values in disabled comboboxes)
https://i.imgur.com/cmjaPoR.png (Single row changes)
Broken behaviour (Usercontrol embedded into tabcontrol, ObservableCollection)
https://i.imgur.com/yC3iAas.png (not loading on window opened)
https://i.imgur.com/aQgPMCN.png (loaded after scroll, including invalid values)
https://i.imgur.com/dqo39aB.png (one combobox changed, all rows change)
XAML for the DataGrid and the misbehaving combobox column (no binding errors):
<DataGrid Grid.Row ="1" Width="Auto" Height="Auto" AlternatingRowBackground="#FBE9D9" AlternationCount="2"
AutoGenerateColumns="False" GridLinesVisibility="None"
ItemsSource="{Binding Familiars}" IsSynchronizedWithCurrentItem="True">
<DataGrid.InputBindings>
<MouseBinding MouseAction="LeftDoubleClick" Command="{Binding OpenDataFamiliarWindow}" CommandParameter="{Binding Familiars/}"/>
</DataGrid.InputBindings>
<DataGrid.Resources>
<local_c:OwnedToBoolConverter x:Key="OwnedToBoolConverter"/>
<local_c:EnemyTypeToColourConverter x:Key="EnemyTypeToColour"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent"/>
</DataGrid.Resources>
<DataGridTemplateColumn Header="Location" Width="Auto" IsReadOnly="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding AvailableLocationTypes}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MinWidth="90"
SelectedItem="{Binding Info.Location, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsSynchronizedWithCurrentItem="True" IsEnabled="{Binding Info.Owned, Converter={StaticResource OwnedToBoolConverter}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource DescConverter}, Mode=OneTime}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid>
The class that the UserControl is designed to bind to (and the only bit that differs from the working use of essentially the same datagrid):
public class ColiseumVenue : INotifyPropertyChanged
{
public BitmapImage HeaderImage { get; private set; }
public string Name { get; private set; }
public ObservableCollection<FamiliarViewModel> Familiars { get; private set; } = new ObservableCollection<FamiliarViewModel>();
private IModel m_Model;
public ColiseumVenue(IModel model, string name, IEnumerable<FamiliarViewModel> familiars)
{
Name = name;
m_Model = model;
HeaderImage = ImageLoader.LoadImage(Path.Combine(ApplicationPaths.GetViewIconDirectory(), name + ".png"));
foreach(var familiar in familiars)
{
Familiars.Add(familiar);
}
}
public ColiseumView Window { get; set; }
private BaseCommand m_openDataFamiliarWindow;
public ICommand OpenDataFamiliarWindow
{
<snip>
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
The public variables of the FamiliarViewModel class are:
public class FamiliarViewModel : INotifyPropertyChanged
{
public FamiliarInfo Info { get; set; }
public LocationTypes[] AvailableLocationTypes { get; private set; }
public OwnershipStatus[] AvailableOwnershipStatuses { get; private set; }
public BondingLevels[] AvailableBondingLevels { get; private set; }
public BookmarkState[] AvailableBookmarkStates { get; private set; }
}
The parts of the FamiliarInfo class that are bound to are:
public ICRUD<OwnedFamiliar> OwnedFamiliar
{
get { return m_OwnedFamiliar; }
set
{
if(m_OwnedFamiliar != value)
{
m_OwnedFamiliar = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Owned"));
}
}
}
public OwnershipStatus Owned => OwnedFamiliar != null ? OwnershipStatus.Owned : OwnershipStatus.NotOwned;
public BondingLevels? BondLevel
{
get
{
return OwnedFamiliar?.Fetch()?.BondingLevel;
}
set
{
if (value.HasValue)
{
OwnedFamiliar?.Update(f => f.BondingLevel = value.Value);
}
}
}
public LocationTypes? Location
{
get
{
return OwnedFamiliar?.Fetch()?.Location;
}
set
{
if (value.HasValue)
{
OwnedFamiliar?.Update(f => f.Location = value.Value);
}
}
}
This turned out to be the same issue as: WPF MVVM DataGridComboboxColumn change one row updates all
The selected answer (setting IsSynchronizedWithCurrentItem to null) worked.

get selected menuitem wpf

I have a menu and some submenus
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel { Header = "Select a Building",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel { Header = "Building 4",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel { Header = "< 500",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel {Header = "Executives" },
new MenuItemViewModel {Header = "Engineers" },
new MenuItemViewModel {Header = "Sales" },
new MenuItemViewModel {Header = "Marketing"},
new MenuItemViewModel {Header = "Support"}
}
},
new MenuItemViewModel { Header = "500 - 999",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel {Header = "Executives" },
new MenuItemViewModel {Header = "Engineers" },
new MenuItemViewModel {Header = "Sales" },
new MenuItemViewModel {Header = "Marketing"},
new MenuItemViewModel {Header = "Support"}
}
}
}
}
}
I am trying to capture the value of each selection the user makes and display them in a listbox. For example a user selects "Building 4" then "500 - 999" then "support" as those values are selected they populated the list box. I have a function in MenuItemViewModel that is called Execute(), this will get the Header of the last value selected, I.e "support" but I cannot figure out how to get that value to the listbox.
Here is the ViewModel
public class MenuItemViewModel
{
private readonly ICommand _command;
string Value;
public ObservableCollection<Cafe> Cafes
{
get;
set;
}
public MenuItemViewModel()
{
_command = new CommandViewModel(Execute);
}
public string Header { get; set; }
public ObservableCollection<MenuItemViewModel> MenuItems { get; set; }
public ICommand Command
{
get
{
return _command;
}
}
public void Execute()
{
MessageBox.Show("Clicked at " + Header);
}
}
And finally the xaml for the MenuItem and ListBox:
<Menu x:Name="buildingMenu" Margin="0,15,0,0" HorizontalAlignment="Left" Height="20" Width="200" ItemsSource="{Binding MenuItems}" >
<Menu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Command}" />
</Style>
</Menu.ItemContainerStyle>
<Menu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=MenuItems}">
<TextBlock Text="{Binding Header}"/>
</HierarchicalDataTemplate>
</Menu.ItemTemplate>
</Menu>
<ListBox Name="selectionListBox" HorizontalAlignment="Right" Height="179" Margin="0,177,55,0" VerticalAlignment="Top" Width="500" />
I have tried to add the Header to a List in the ViewModel but I cannot get anything work. Is there something similar to a combobox's SelectedValue that can be used here? Thanks for the help
This is actually a lot harder to do than you might expect, because you're using menus in a way they're not really designed to be used.
First of all you're going to need a list of items in your view model to bind to, eg:
public ObservableCollection<string> ListItems { get; set; }
And the corresponding binding in your ListBox:
<ListBox ItemsSource="{Binding ListItems}" Name="selectionListBox" />
Next, you'll need to populate this list with items as they're being clicked. This is tricky with the way you've done things at the moment because ListItems will need to be in your main view model, yet your command handler is in MenuItemViewModel. This means you'll either need to add a way for MenuItemViewModel to call it's parent, or better yet move the command handler to the main view model (so that it's now shared) and have the MenuItems pass in their data context object. I use MVVM Lite and in that framework you do something like this:
private ICommand _Command;
public ICommand Command
{
get { return (_Command = (_Command ?? new RelayCommand<MenuItemViewModel>(Execute))); }
}
public void Execute(MenuItemViewModel menuItem)
{
// ... do something here...
}
You don't seem to be using MVVM Lite, but whatever your framework it is it will have support for passing a parameter into your execute function, so I'll leave that for you to look up. Either way your MenuItem Command bindings will now all need to be modified to point to this common handler:
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding ElementName=buildingMenu, Path=DataContext.Command}" />
</Style>
Your other requirement is for the list to populate as each submenu item is selected, and this is where things get nasty. Submenus don't trigger commands when they close, they trigger SubmenuOpened and SubmenuClosed events. Events can't bind to the command handlers in your view model, so you have to use code-behind handlers instead. If you were declaring your MenuItems explicitly in XAML you could just set them directly there, but your Menu is binding to a collection, so your MenuItems are being populated by the framework and you'll have to set the handler by adding an EventSetter to your style:
<Style TargetType="{x:Type MenuItem}">
<EventSetter Event="SubmenuOpened" Handler="OnSubMenuOpened" />
<Setter Property="Command" Value="{Binding ElementName=buildingMenu, Path=DataContext.Command}" />
</Style>
This works, in that calls the specified handler function in your MainWindow code-behind:
private void OnSubmenuOpened(object sender, RoutedEventArgs e)
{
// uh oh, what now?
}
The problem of course is that event handlers exist in the view, but in MVVM we need it in the view model. If you're happy to corrupt your views like this for the sake of getting your application to work then check this SO question for some example code showing how to get it working. However, if you're a hard-core MVVM purist like myself you'll probably want a solution that doesn't involve code-behind, and in that case you'll again need to refer to your framework. MVVM Lite provides a behavior that allows you to convert events to command, you typically use it like this:
<MenuItem>
<i:Interaction.Triggers xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
<i:EventTrigger EventName="OnSubmenuOpened">
<cmd:EventToCommand Command="{Binding SubmenuOpenedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
The problem with behaviours though is that you can't set them in a style. Styles are applied to all objects, which is why EventSetter works fine. Behaviours have to be created for each object that they're used for.
So the final piece of the puzzle is that if you have cases in MVVM where you need to set a behaviour in a style then you'll need to use the solution posted by vspivak in his article Using System.Windows.Interactivity Behaviors and Actions in WPF/Silverlight styles. I've used this myself in commercial projects, and it's a nice general solution to the bigger problem of redirecting event handlers to command handlers in styles.
Hope that answers your question. I'm sure it seems ridiculously convoluted, but what you're trying to do is a fairly pathological scenario well outside how WPF is typically used.
Put a Click event on any MenuItem that is created and when the event is fired, acquire the data item off of the source's DataContext which seeded the MenuItem.
Example
I created a recent list of operation which were needed as submenu's under a Recent menu. I seeded the sub list as a model of typeMRU which was used to load the menu *via ItemsSource and held in an ObservableCollection<MRU>() MRUS :
Xaml
<MenuItem Header="Recent"
Click="SelectMRU"
ItemsSource="{Binding Path=MRUS}">
...
</MenuItem>
Codebehind
private void SelectMRU(object sender, RoutedEventArgs e)
{
try
{
var mru = (e.OriginalSource as MenuItem).DataContext as MRU;
if (mru is not null)
{
VM.ClearAll();
this.Title = mru.Name;
ShowJSON(File.ReadAllText(mru.Address));
}
}
catch (Exception ex)
{
VM.Error = ex.Demystify().ToString();
}
}
public class MRU
{
public string Name { get; set; }
public string Address { get; set; }
public string Data { get; set; }
public bool IsValid => !string.IsNullOrWhiteSpace(Name);
public bool IsFile => !string.IsNullOrWhiteSpace(Address);
public bool IsData => !IsFile;
public MRU(string address)
{
if (string.IsNullOrWhiteSpace(address)
|| !File.Exists(address)) return;
Address = address;
Name = System.IO.Path.GetFileName(address);
}
// This will be displayed as a selectable menu item.
public override string ToString() => Name;
}
Note that the full Menu code can be viewed in public gist Gradient Menu Example with MRU

Nested ObservableCollection data binding in WPF

I am very new to WPF and trying to create a self learning application using WPF.
I am struggling to understand concepts like data binding,data templates,ItemControls to a full extent.
I am trying to create a learn page with the following requirements in mind.
1) The page can have more than one question.A scroll bar should be displayed once the
questions fills up the whole page.
2) The format of the choices vary based on the question type.
3) the user shall be able to select the answer for the question.
I am facing problem with binding nested ObservableCollection and displaying the content as for the above requirements.
Can someone help in how to create a page as shown below and how to use INotifyPropertyChanged along XMAL to do the nested binding.
Here is the basic code I am trying to use to use display the questions and answers.
namespace Learn
{
public enum QuestionType
{
OppositeMeanings,
LinkWords
//todo
}
public class Question
{
public Question()
{
Choices = new ObservableCollection<Choice>();
}
public string Name { set; get; }
public string Instruction { set; get; }
public string Clue { set; get; }
public ObservableCollection<Choice> Choices { set; get; }
public QuestionType Qtype { set; get; }
public Answer Ans { set; get; }
public int Marks { set; get; }
}
}
namespace Learn
{
public class Choice
{
public string Name { get; set; }
public bool isChecked { get; set; }
}
}
namespace Learn
{
public class NestedItemsViewModel
{
public NestedItemsViewModel()
{
Questions = new ObservableCollection<Question>();
for (int i = 0; i < 10; i++)
{
Question qn = new Question();
qn.Name = "Qn" + i;
for (int j = 0; j < 4; j++)
{
Choice ch = new Choice();
ch.Name = "Choice" + j;
qn.Choices.Add(ch);
}
Questions.Add(qn);
}
}
public ObservableCollection<Question> Questions { get; set; }
}
public partial class LearnPage : UserControl
{
public LearnPage()
{
InitializeComponent();
this.DataContext = new NestedItemsViewModel();
}
}
}
You initial attempt gets you 80% of the way there. Hopefully, my answer will get you a little closer.
To start with, INotifyPropertyChanged is an interface an object supports to notify the Xaml engine that data has been modified and the user interface needs to be updated to show the change. You only need to do this on standard clr properties.
So if your data traffic is all one way, from the ui to the model, then there is no need for you to implement INotifyPropertyChanged.
I have created an example that uses the code you supplied, I have modified it and created a view to display it. The ViewModel and the data classes are as follows
public enum QuestionType
{
OppositeMeanings,
LinkWords
}
public class Instruction
{
public string Name { get; set; }
public ObservableCollection<Question> Questions { get; set; }
}
public class Question : INotifyPropertyChanged
{
private Choice selectedChoice;
private string instruction;
public Question()
{
Choices = new ObservableCollection<Choice>();
}
public string Name { set; get; }
public bool IsInstruction { get { return !string.IsNullOrEmpty(Instruction); } }
public string Instruction
{
get { return instruction; }
set
{
if (value != instruction)
{
instruction = value;
OnPropertyChanged();
OnPropertyChanged("IsInstruction");
}
}
}
public string Clue { set; get; }
public ObservableCollection<Choice> Choices { set; get; }
public QuestionType Qtype { set; get; }
public Choice SelectedChoice
{
get { return selectedChoice; }
set
{
if (value != selectedChoice)
{
selectedChoice = value;
OnPropertyChanged();
}
}
}
public int Marks { set; get; }
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Choice
{
public string Name { get; set; }
public bool IsCorrect { get; set; }
}
public class NestedItemsViewModel
{
public NestedItemsViewModel()
{
Questions = new ObservableCollection<Question>();
for (var h = 0; h <= 1; h++)
{
Questions.Add(new Question() { Instruction = string.Format("Instruction {0}", h) });
for (int i = 1; i < 5; i++)
{
Question qn = new Question() { Name = "Qn" + ((4 * h) + i) };
for (int j = 0; j < 4; j++)
{
qn.Choices.Add(new Choice() { Name = "Choice" + j, IsCorrect = j == i - 1 });
}
Questions.Add(qn);
}
}
}
public ObservableCollection<Question> Questions { get; set; }
internal void SelectChoice(int questionIndex, int choiceIndex)
{
var question = this.Questions[questionIndex];
question.SelectedChoice = question.Choices[choiceIndex];
}
}
Notice that Answer has been changed to a SelectedChoice. This may not be what you require but it made the example a little easier. i have also implemented the INotifyPropertyChanged pattern on the SelectedChoice so I can set the SelectedChoice from code (notably from a call to SelectChoice).
The main windows code behind instantiates the ViewModel and handles a button event to set a choice from code behind (purely to show INotifyPropertyChanged working).
public partial class MainWindow : Window
{
public MainWindow()
{
ViewModel = new NestedItemsViewModel();
InitializeComponent();
}
public NestedItemsViewModel ViewModel { get; set; }
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.SelectChoice(3, 3);
}
}
The Xaml is
<Window x:Class="StackOverflow._20984156.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:learn="clr-namespace:StackOverflow._20984156"
DataContext="{Binding RelativeSource={RelativeSource Self}, Path=ViewModel}"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<learn:SelectedItemIsCorrectToBooleanConverter x:Key="SelectedCheckedToBoolean" />
<Style x:Key="ChoiceRadioButtonStyle" TargetType="{x:Type RadioButton}" BasedOn="{StaticResource {x:Type RadioButton}}">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource SelectedCheckedToBoolean}">
<Binding Path="IsCorrect" />
<Binding RelativeSource="{RelativeSource Self}" Path="IsChecked" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="Green"></Setter>
</DataTrigger>
<DataTrigger Value="False">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource SelectedCheckedToBoolean}">
<Binding Path="IsCorrect" />
<Binding RelativeSource="{RelativeSource Self}" Path="IsChecked" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="Red"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<DataTemplate x:Key="InstructionTemplate" DataType="{x:Type learn:Question}">
<TextBlock Text="{Binding Path=Instruction}" />
</DataTemplate>
<DataTemplate x:Key="QuestionTemplate" DataType="{x:Type learn:Question}">
<StackPanel Margin="10 0">
<TextBlock Text="{Binding Path=Name}" />
<ListBox ItemsSource="{Binding Path=Choices}" SelectedItem="{Binding Path=SelectedChoice}" HorizontalAlignment="Stretch">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type learn:Choice}">
<RadioButton Content="{Binding Path=Name}" IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}, Path=IsSelected}" Margin="10 1"
Style="{StaticResource ChoiceRadioButtonStyle}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</Window.Resources>
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom">
<Button Content="Select Question 3 choice 3" Click="ButtonBase_OnClick" />
</StackPanel>
<ItemsControl ItemsSource="{Binding Path=Questions}">
<ItemsControl.ItemTemplateSelector>
<learn:QuestionTemplateSelector QuestionTemplate="{StaticResource QuestionTemplate}" InstructionTemplate="{StaticResource InstructionTemplate}" />
</ItemsControl.ItemTemplateSelector>
</ItemsControl>
</DockPanel>
</Window>
Note: My learn namespace is different from yours so if you use this code, you will need to modify it to your namespace.
So, the primary ListBox display a list of Questions. Each item in the ListBox (each Question) is rendered using a DataTemplate. Similarly, in the DataTemplate, a ListBox is used to display the choices and a DataTemplate is used to render each choice as a radio button.
Points of interest.
Each choice is bound to the IsSelected property of the ListBoxItem it belongs to. It may not appear in the xaml but there will be a ListBoxItem for each choice. The IsSelected property is kept in sync with the SelectedItem property of the ListBox (by the ListBox) and that is bound to the SelectedChoice in your question.
The choice ListBox has an ItemsPanel. This allows you to use the layout strategy of a different type of panel to layout the items of the ListBox. In this case, a horizontal StackPanel.
I have added a button to set the choice of question 3 to 3 in the viewmodel. This will show you INotifyPropertyChanged working. If you remove the OnPropertyChanged call from the setter of the SelectedChoice property, the view will not reflect the change.
The example above does not handle the Instruction Type.
To handle instructions, I would either
Insert the instruction as a question and change the question DataTemplate so it does not display the choices for an instruction; or
Create a collection of Instructions in the view model where the Instruction type has a collection of questions (the view model would no longer have a collection of questions).
The Instruction class would be something like
public class Instruction
{
public string Name { get; set; }
public ObservableCollection<Question> Questions { get; set; }
}
Addition based on comment regarding timer expiration and multiple pages.
The comments here are aimed at giving you enough information to know what to search for.
INotifyPropertyChanged
If in doubt, implement INotifyPropertyChanged. My comment above was to let you know why you use it. If you have data already displayed that will be manipulated from code, then you must implement INotifyPropertyChanged.
The ObservableCollection object is awesome for handling the manipulation of lists from code. Not only does it implement INotifyPropertyChanged, but it also implements INotifyCollectionChanged, both of these interfaces ensure that if the collection changes, the xaml engine knows about it and displays the changes. Note that if you modify a property of an object in the collection, it will be up to you to notify the Xaml engine of the change by implementing INotifyPropertyChanged on the object. The ObservableCollection is awesome, not omnipercipient.
Paging
For your scenario, paging is simple. Store the complete list of questions somewhere (memory, database, file). When you go to page 1, query the store for those questions and populate the ObservableCollection with those questions. When you go to page 2, query the store for page 2 questions, CLEAR the ObservableCollection and re populate. If you instantiate the ObservableCollection once and then clear and repopulate it while paging, the ListBox refresh will be handled for you.
Timers
Timers are pretty resource intensive from a windows point of view and as such, should be used sparingly. There are a number of timers in .net you can use. I tend to play with System.Threading.Timer or System.Timers.Timer. Both of these invoke the timer callback on a thread other than the DispatcherThread, which allows you to do work without affecting the UI responsiveness. However, if during the work you need to modify the UI, you will need to Dispatcher.Invoke or Dispatcher.BeginInvoke to get back on the Dispatcher thread. BeginInvoke is asynchronous and therefore, should not hang the thread while it waits for the DispatcherThread to become idle.
Addition based on comment regarding separation of data templates.
I added an IsInstruction to the Question object (I did not implement a Instruction class). This shows an example of raising the PropertyChanged event from property A (Instruction) for Property B (IsInstruction).
I moved the DataTemplate from the list box to the Window.Resources and gave it a key. I also created a second DataTemplate for the instruction items.
I created a DataTemplateSelector to choose which DataTemplate to use. DataTemplateSelectors are good when you need to select a DataTemplate as the Data is being loaded. Consider it a OneTime selector. If you required the DataTemplate to change during the scope of the data it is rendering, then you should use a trigger. The code for the selector is
public class QuestionTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
DataTemplate template = null;
var question = item as Question;
if (question != null)
{
template = question.IsInstruction ? InstructionTemplate : QuestionTemplate;
if (template == null)
{
template = base.SelectTemplate(item, container);
}
}
else
{
template = base.SelectTemplate(item, container);
}
return template;
}
public DataTemplate QuestionTemplate { get; set; }
public DataTemplate InstructionTemplate { get; set; }
}
The selector is bound to the ItemTemplateSelector of the ItemsControl.
Finally, I converted the ListBox into an ItemsControl. The ItemsControl has most of the functionality of a ListBox (the ListBox control is derived from an ItemsControl) but it lacks the Selected functionality. It will make your questions seem more like a page of questions than a list.
NOTE: Although I only added the code of the DataTemplateSelector to the addition, I updated the code snipits throughout the rest of the answer to work with the new DataTemplateSelector.
Addition based on comment regarding setting the background for right and wrong answers
Setting the background dynamically based on values in the model requires a trigger, in this case, multiple triggers.
I have updated the Choice object to include an IsCorrect and during the creation of the questions in the ViewModel, I have assigned IsCorrect on one of the Choices for each answer.
I have also updated the MainWindow to include style triggers on the RadioButton. There are a few points to not about triggers
1. The style or the RadioButton sets the backgound when the mouse is over. A fix would require recreating the style of the RadioButton.
1. Since the trigger is based on 2 values, we can either create another property on the model to combine the 2 properties, or use MultiBinding and a MultValueConverter. I have used the MultiBinding and the MultiValueConverter is as follows.
public class SelectedItemIsCorrectToBooleanConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var boolValues = values.OfType<bool>().ToList();
var isCorrectValue = boolValues[0];
var isSelected = boolValues[1];
if (isSelected)
{
return isCorrectValue;
}
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
I hope this helps

Adding TreeView in WPF with context menu in subitems

In my WPF application, I want to add a TreeView control. The tree view control needs to be populated with items from database. So I bind the ItemsSource property to string collection.
Every item in the tree control can have from 0 to 32 child items. Again these items need to be binded. Each of these sub items should have a context menu with two options "Rename" and "Delete". How can I do this in WPF?
There are a few ways to do this. Here's one way that applies the context menu using a trigger that is bound to a property IsLeaf on the underlying view model.
MainWindow.xaml:
<Window x:Class="WpfScratch.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<!-- the context menu for all tree view items -->
<ContextMenu x:Key="TreeViewItemContextMenu">
<MenuItem Header="Rename" />
<MenuItem Header="Delete" />
</ContextMenu>
<!-- the data template for all tree view items -->
<HierarchicalDataTemplate x:Key="TreeViewItemTemplate" ItemsSource="{Binding Nodes}">
<TextBlock x:Name="TextBlock" Text="{Binding Text}" />
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsLeaf}" Value="True">
<Setter TargetName="TextBlock" Property="ContextMenu" Value="{StaticResource TreeViewItemContextMenu}" />
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</Window.Resources>
<!-- the treeview -->
<TreeView DataContext="{Binding TreeView}"
ItemsSource="{Binding Nodes}"
ItemTemplate="{StaticResource TreeViewItemTemplate}">
</TreeView>
</Window>
MainWindow.xaml.cs:
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowModel(
new MainWindowTreeViewModel(
new MainWindowTreeViewNodeModel(
"1",
new MainWindowTreeViewNodeModel("A"),
new MainWindowTreeViewNodeModel("B"),
new MainWindowTreeViewNodeModel("C")),
new MainWindowTreeViewNodeModel(
"2",
new MainWindowTreeViewNodeModel("A"),
new MainWindowTreeViewNodeModel("B"),
new MainWindowTreeViewNodeModel("C")),
new MainWindowTreeViewNodeModel(
"3",
new MainWindowTreeViewNodeModel("A"),
new MainWindowTreeViewNodeModel("B"),
new MainWindowTreeViewNodeModel("C"))));
}
}
MainWindowModel.cs:
public class MainWindowModel
{
public MainWindowModel(MainWindowTreeViewModel treeView)
{
TreeView = treeView;
}
public MainWindowTreeViewModel TreeView { get; private set; }
}
public class MainWindowTreeViewModel
{
public MainWindowTreeViewModel(params MainWindowTreeViewNodeModel[] nodes)
{
Nodes = nodes.ToList().AsReadOnly();
}
public ReadOnlyCollection<MainWindowTreeViewNodeModel> Nodes { get; private set; }
}
public class MainWindowTreeViewNodeModel
{
public MainWindowTreeViewNodeModel(string text, params MainWindowTreeViewNodeModel[] nodes)
{
Text = text;
Nodes = nodes.ToList().AsReadOnly();
}
public string Text { get; private set; }
public ReadOnlyCollection<MainWindowTreeViewNodeModel> Nodes { get; private set; }
public bool IsLeaf { get { return Nodes.Count == 0; } }
}

Binding ObservableCollection of objects trouble

I'm trying to code an rssreader and would be pleased for some architecture hints.
My reader main window hosts two wpf pages which are loaded into frames, it's a "bottombar" where user can select different rss providers. In the main frame (or page) is my listview.
Because of an loading animation and UI Freeze I've an extra class with a backgroundworker which fills an observable collection with RSS Data, when I'm debugging, it fills my collection correctly.
In main page i'm setting the datacontext to this observable collection but listview doesn't show anything, here I'm stuck.
That's what I have:
MainPage XAML:
> <ListBox ItemsSource="{Binding}" DisplayMemberPath="RssTitle"
> IsSynchronizedWithCurrentItem="True"
> SelectionChanged="itemsList_SelectionChanged"
> ItemContainerStyle="{DynamicResource listboxitem_style}" Height="396"
> HorizontalAlignment="Left" Margin="126,12,0,0" Name="ListBox1"
> VerticalAlignment="Top" Width="710"></ListBox>
ListBox1.DataContext = GetRssItems.observable_list;
Bottompage to get another rss feed:
GetRssItems getitems = new GetRssItems();
GetRssItems.observable_collection = null;
getitems.start_bg_worker("url");
GetRssItems.cs
public class GetRssItems
{
public static ObservableCollection<RSSItem> observable_collection { get; set; }
public static string tmp_url;
public BackgroundWorker worker = new BackgroundWorker();
public void start_bg_worker(string url)
{
if (!worker.IsBusy)
{
worker.DoWork += new DoWorkEventHandler(worker_DoWork);
worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
worker.RunWorkerAsync(url);
}
}
}
In BackgroundWorkers DoWork I'm receiving rss items with linq and add it to my observable collection:
observable_collection.Add(new RSSItem(item.tmp_Title, item.tmp_Link, item.tmp_Description, item.tmp_pubDate, item.tmp_ImageUrl));
Seperate class RSSItem.cs
public class RSSItem
{
public string RssTitle { get; set; }
public string RssLink { get; set; }
public string RssDescription { get; set; }
public string RsspubDate { get; set; }
public string RssImageUrl { get; set; }
public RSSItem(string rsstitle, string rsslink, string rssdescription, string rsspubdate, string rssimageurl)
{
RssTitle = rsstitle;
RssLink = rsslink;
RssDescription = rssdescription;
RsspubDate = rsspubdate;
RssImageUrl = rssimageurl;
}
}
Thanks for your time and hints.
Best Regards
You need to read up a bit MVVM to get the most benefit from WPF. Your line setting the listbox's datacontext is rather confusing.
What you should have is your main window's (xaml) data context set to a view model class that contains your observable collection. The list box's ItemsSource is set to that property name.
For example:
public class MainViewModel : INotifyPropertyChanged
{
public ObservableCollection<RSSItem> RSSItems
{
get;
set;
}
// Other stuff applicable to the main window.
}
When the view is constructed, pass an instance of the MainViewModel to it's DataContext. Then the Xaml for the ListBox would be:
<ListBox ItemsSource={Binding Path=RSSItems} ... />
If you want to be able to set/change the RSSItems collection instance (I.e. public setter) then you should set it up it's setter with the NotifyPropertyChanged event, however if you just add/remove items then this shouldn't be necessary. (I.e. loading populating the items in the constructor.)
use the following:
the data context should be the Object getitems
<ListBox ItemsSource="{Binding observable_collection}" Height="167" Margin="0" Name="listBox1" Width="330" FontSize="24" HorizontalAlignment="Center" VerticalAlignment="Top">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding RssTitle}" FontWeight="Bold" FontSize="16" />
<TextBlock Text="{Binding RssLink}" FontSize="16"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
PS:
your naming is HORRBILE

Categories