I want to set an event for every TreeViewItem i'm creating dynamically in code. I'm using the following XAML code:
<Window.Resources>
<Style TargetType="TreeViewItem">
<EventSetter Event="Selected" Handler="TreeViewItemSelectionEvent"/>
<EventSetter Event="MouseRightButtonDown" Handler="TreeViewItemRightClickEvent"/>
</Style>
</Window.Resources>
But this only works on a root TreeViewItem in the TreeView. If i click on another item, which is a child of the root, then i allways get back the root.
Can i make this work somehow? I'd love this method, thus you don't need to handle the events in your code which makes it look cleaner.
TL;DR: Use HierarchicalDataTemplate and Styles to display data in a TreeView. Dynamically loading data in your viewmodel will automatically update the TreeView.
Do not manually create TreeViewItems.
In MVVM, what matters the most is the data and the architecture of your data. The general pattern is to define your data and separately define how the view will display your data. Your view is dependent on your data, not the other way around.
So let's create a ProductViewModel which has a ProductName, a list of sub products and a IsSelected property. We equip this class with a method LoadSubProductsCollectionFromDataSource which retrieves data from whatever data source you may have. Here, I just load somme dummy items.
public class ProductViewModel {
/// <summary>
/// Backing field for the IsSelected property
/// </summary>
private bool _isSelected;
/// <summary>
/// Gets or sets the collection of materials used to build this Product.
/// </summary>
public ObservableCollection<ProductViewModel> SubProducts { get; set; } = new ObservableCollection<ProductViewModel>();
/// <summary>
/// Gets or sets the name of this product.
/// </summary>
public string ProductName { get; set; }
/// <summary>
/// Gets or sets the selected state of this product.
/// </summary>
public bool IsSelected {
get => _isSelected;
set {
//The product has been selected or deselected.
if (!_isSelected && SubProducts.Count == 0) {
//We load data into it if not already done.
LoadSubProductsCollectionFromDataSource();
}
_isSelected = value;
}
}
/// <summary>
/// Loads sub products data into this product.
/// </summary>
private void LoadSubProductsCollectionFromDataSource() {
//..
//Do stuff to retrieve your data dynamically and
//add them to the SubProducts collection.
//...
for (int i = 0; i < 5; i++) {
//Add dummy items
SubProducts.Add(new ProductViewModel() { ProductName = "Some product " + i.ToString() });
}
}
}
In your MainWindow.xaml.cs, initialize and expose a collection of view model objects like this:
public partial class MainWindow : Window {
/// <summary>
/// Exposes the root product of the tree
/// </summary>
public ObservableCollection<ProductViewModel> RootProducts { get; } = new ObservableCollection<ProductViewModel>();
public MainWindow() {
InitializeComponent();
RootProducts.Add(new ProductViewModel() { ProductName = "Root product" });
}
}
This collection would normally be stored in a main viewmodel object but for simplicity I just created it in the MainWindow. Notice how I expose it as a property (to allow Binding) and as an ObservableCollection (to automatically notify the view when the collection changes).
Finally, tell your view how to display your ProductViewModel objects using a TreeView:
<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"
x:Name="window"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<!--Tell the treeview how to hierarchically display and organize ProductViewModel items-->
<HierarchicalDataTemplate DataType="{x:Type local:ProductViewModel}" ItemsSource="{Binding SubProducts}">
<TextBlock Text="{Binding ProductName}"></TextBlock>
</HierarchicalDataTemplate>
<!--Tell each treeviewitem to bind IsSelected to the PoductViewModel.ISSelected property.-->
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"></Setter>
</Style>
</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding ElementName=window, Path=RootProducts}"/>
</Grid>
</Window>
Now, every time you select a TreeViewItem in your TreeView, its IsSelected property is set to true (this is the behavior of a TreeViewItem). Because of our binding, it also sets to true the IsSelected property of the corresponding ProductViewModel. In the setter of this property, we make a call to populate the list of subproducts. Because this list is actually an ObservableCollection, it notifies the View (the TreeView) which knows it should update itself with new TreeViewItems.
Related
I have a (WPF) Catel DataWindow with a DataGrid, where the SelectedItem property is bound to a VM property, and has two buttons indented to launch different Catel TaskCommands on the selected data grid item.
Note the CommandParameters are bound in different ways to what seems - but isn't - the same value:
<catel:DataWindow x:Class="CatelWPFApplication1.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:catel="http://schemas.catelproject.com"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:CatelWPFApplication1.ViewModels"
mc:Ignorable="d"
ShowInTaskbar="True"
ResizeMode="NoResize"
SizeToContent="WidthAndHeight"
WindowStartupLocation="Manual"
d:DataContext="{d:DesignInstance Type=viewModels:MainWindowViewModel, IsDesignTimeCreatable=True}"
>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="0,6">
<Button Command="{Binding ViaVmCommand}"
CommandParameter="{Binding SelectedPerson, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Content="Binding via VM" />
<Button Command="{Binding ViaElementNameCommand}"
CommandParameter="{Binding SelectedItem, ElementName=PersonDataGrid, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Content="Binding via ElementName"
x:Name="ViaElementButton"/>
</StackPanel>
<DataGrid x:Name="PersonDataGrid"
ItemsSource="{Binding PersonList}"
SelectedItem="{Binding SelectedPerson, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
AutoGenerateColumns="True" />
</StackPanel>
</catel:DataWindow>
For completeness, the VM code is given below.
When the Window is first shown, no data grid row is selected, and as expected both buttons are disabled. If the first data grid row gets selected (by a mouse click), only the one button binding to the SelectedPerson property of the VM gets enabled while to my surprise the other one stays disabled. On selecting a different item, both buttons get enabled and on Crtl-Clicking unselecting the selected line, the VM bound button gets disabled while the one binding through the ElementName mechanism doesn't.
Using Debugger breakpoints, I proved that both CanExecute functions are called on window initialization and on each item selection change. Yet the parameter for the Button binding via ElementName reference is one click behind.
If I change the VM SelectedPerson property in one of those commands, both buttons update as expected, their CanExecute handlers get the correct item value.
I see that binding to the VM property isn't bad, as it will be useful elsewhere in the business logic, yet I like learn why the two approaches behave so different.
What is going on with the 'ElementName' binding, why is it one click behind?
Finally, this is the VM
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Catel.Data;
using Catel.MVVM;
using CatelWPFApplication1.Models;
namespace CatelWPFApplication1.ViewModels
{
/// <summary>
/// MainWindow view model.
/// </summary>
public class MainWindowViewModel : ViewModelBase
{
#region Fields
#endregion
#region Constructors
public MainWindowViewModel()
{
ViaVmCommand = new TaskCommand<Person>(OnViaVmCommandExecute, OnViaVmCommandCanExecute);
ViaElementNameCommand = new TaskCommand<Person>(OnViaElementNameCommandExecute, OnViaElementNameCommandCanExecute);
PersonList = new List<Person>();
PersonList.Add(new Person(){FirstName = "Albert", LastName = "Abraham"});
PersonList.Add(new Person(){FirstName = "Betty", LastName = "Baboa"});
PersonList.Add(new Person(){FirstName = "Cherry", LastName="Cesar"});
}
#endregion
#region Properties
/// <summary>
/// Gets the title of the view model.
/// </summary>
/// <value>The title.</value>
public override string Title { get { return "View model title"; } }
public List<Person> PersonList { get; }
// classic Catel property, avoiding any magic with Fody weavers
public Person SelectedPerson
{
get { return GetValue<Person>(SelectedPersonProperty); }
set
{
SetValue(SelectedPersonProperty, value);
}
}
public static readonly PropertyData SelectedPersonProperty = RegisterProperty(nameof(SelectedPerson), typeof(Person), null);
#endregion
#region Commands
public TaskCommand<Person> ViaElementNameCommand { get; }
private bool OnViaElementNameCommandCanExecute(Person person)
{
return person is not null;
}
private async Task OnViaElementNameCommandExecute(Person person)
{
SelectedPerson = null;
}
public TaskCommand<Person> ViaVmCommand { get; }
private bool OnViaVmCommandCanExecute(Person person)
{
return person is not null;
}
private async Task OnViaVmCommandExecute(Person person)
{
SelectedPerson = PersonList.FirstOrDefault();
}
#endregion
}
}
I think this is caused by the moment the commands get (re)evaluated.
You probably have the InvalidateCommandsOnPropertyChange property set to true (default value). For more info, see https://github.com/catel/catel/blob/develop/src/Catel.MVVM/MVVM/ViewModels/ViewModelBase.cs#L1016
At this stage, the binding probably didn't have a chance yet to update itself and is thus sending the previous version.
A workaround for this issue could be to use the dispatcher service inside the VM to re-evaluate the commands yourself:
In the ctor:
private readonly IDispatcherService _dispatcherService;
public MainWindowViewModel(IDispatcherService dispatcherService)
{
Argument.IsNotNull(() => dispatcherService);
_dispatcherService = dispatcherService;
}
Override the OnPropertyChanged:
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
// Don't alter current behavior by calling base
base.OnPropertyChanged(e);
// Dispatcher so the bindings get a chance to update
_dispatcherService.BeginInvoke(await () =>
{
await Task.Delay(10);
ViewModelCommandManager.InvalidateCommands();
});
}
Dislaimer: this is code written without an editor, but hopefully it gives you the idea what it should do
I have searched a lot but cannot get what I want. I need to fill a combo box with images (114 images embedded in Resources.resx).
I am just getting list, not images. Here is my code.
ResourceSet rsrcSet =MyProject.Properties.Resources.ResourceManager.GetResourceSet(CultureInfo.CurrentCulture, true, true);
List<object> images = new List<object>();
foreach (DictionaryEntry entry in rsrcSet)
{
//String name = entry.Key.ToString();
//Object resource = entry.Value;
images.Add( Don't know what will be here? );
}
var comboBox = sender as ComboBox;
comboBox.ItemsSource = images;
and my XAML
<ComboBox HorizontalAlignment="Left" Grid.Column="0" Grid.Row="0" VerticalAlignment="Top" Width="320" Loaded="ComboBox_Loaded" SelectionChanged="ComboBox_SelectionChanged"/>
It's the easiest to use an item template. To do so we define a DataTemplate with DataType String and set it to the ComboBox.ItemTemplate. In order to use String in XAML we need to reference the xmlns:system="clr-namespace:System;assembly=mscorlib" assembly and namespace. For the binding we use a ObservableCollection<string> that holds the relative paths to your images:
View model:
public class ViewModel : INotifyPropertyChanged
{
public TestViewModel()
{
this.ImageSources = new ObservableCollection<string>() { #"Resources\Icons\image.png" };
}
/// <summary>
/// Event fired whenever a child property changes its value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Method called to fire a <see cref="PropertyChanged"/> event.
/// </summary>
/// <param name="propertyName"> The property name. </param>
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private ObservableCollection<string> imageSources;
public ObservableCollection<string> ImageSources
{
get => this.imageSources;
set
{
this.imageSources = value;
OnPropertyChanged();
}
}
}
Xaml:
<Window x:Class="MainWindow"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<Window.DataContext>
<viewModels:ViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Key="ComboBoxItemTemplate" DataType="system:String">
<Image Source="{Binding}" Height="100" Width="100"/>
</DataTemplate>
</Window.Resources>
<Grid>
<StackPanel>
<ComboBox ItemTemplate="{StaticResource ComboBoxItemTemplate}"
ItemsSource="{Binding ImageSources}" />
</StackPanel>
</Grid>
</Window>
To make it work, your dictionary should contain the relative image paths. If not you have to convert. So instead of initializing the ObservableCollection in the constructor, like in the example, you can move the initialization to anywhere else.
I created a new WPF Project in VS2017 and also imported MVVM Light via NuGet.
Then I added some code that should change the background color from the MainWindows Grid every 25 milliseconds. Sadly that change doesn'T propagate and I have no clue why it doesn't update. Maybe someone here can help me.
Here is the code:
MainViewModel.cs
using GalaSoft.MvvmLight;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace Strober.ViewModel
{
/// <summary>
/// This class contains properties that the main View can data bind to.
/// <para>
/// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel.
/// </para>
/// <para>
/// You can also use Blend to data bind with the tool's support.
/// </para>
/// <para>
/// See http://www.galasoft.ch/mvvm
/// </para>
/// </summary>
public class MainViewModel : ObservableObject
{
private DispatcherTimer timer;
public string Title { get; set; }
private Brush _background;
public Brush Background
{
get
{
return _background;
}
set
{
_background = value;
OnPropertyChanged("Background");
}
}
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel()
{
Background = new SolidColorBrush(Colors.Black);
timer = new DispatcherTimer();
timer.Tick += Timer_Tick;
timer.Interval = new TimeSpan(0, 0, 0,0,100);
timer.Start();
}
private void Timer_Tick(object sender, System.EventArgs e)
{
if (Background == Brushes.Black)
{
Background = new SolidColorBrush(Colors.White);
Title = "White";
}
else
{
Background = new SolidColorBrush(Colors.Black);
Title = "Black";
}
}
#region INotifiedProperty Block
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string PropertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
#endregion
}
}
ViewModelLocator.cs
/*
In App.xaml:
<Application.Resources>
<vm:ViewModelLocator xmlns:vm="clr-namespace:Strober"
x:Key="Locator" />
</Application.Resources>
In the View:
DataContext="{Binding Source={StaticResource Locator}, Path=ViewModelName}"
You can also use Blend to do all this with the tool's support.
See http://www.galasoft.ch/mvvm
*/
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Ioc;
using CommonServiceLocator;
namespace Strober.ViewModel
{
/// <summary>
/// This class contains static references to all the view models in the
/// application and provides an entry point for the bindings.
/// </summary>
public class ViewModelLocator
{
/// <summary>
/// Initializes a new instance of the ViewModelLocator class.
/// </summary>
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
////if (ViewModelBase.IsInDesignModeStatic)
////{
//// // Create design time view services and models
//// SimpleIoc.Default.Register<IDataService, DesignDataService>();
////}
////else
////{
//// // Create run time view services and models
//// SimpleIoc.Default.Register<IDataService, DataService>();
////}
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
public static void Cleanup()
{
// TODO Clear the ViewModels
}
}
}
MainWindow.xaml (MainWindow.xaml.cs is just the regularly generated file)
<Window x:Class="Strober.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:Strober"
mc:Ignorable="d"
DataContext="{Binding Main, Source={StaticResource Locator}}"
Title="{Binding Title}" Height="450" Width="800">
<Grid Background="{Binding Background}">
</Grid>
</Window>
App.xaml
<Application x:Class="Strober.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Strober" StartupUri="MainWindow.xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" d1p1:Ignorable="d" xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006">
<Application.Resources>
<ResourceDictionary>
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" xmlns:vm="clr-namespace:Strober.ViewModel" />
</ResourceDictionary>
</Application.Resources>
</Application>
greg
The primary issue in your code is that System.Windows.Media.SolidColorBrush does not override the Equals() method, and so your expression Background == Brushes.Black is never true. Since you are creating explicit new instances of the SolidColorBrush object, and since the == operator is just comparing the instance references, the comparison between your brush value and the built-in Brushes.Black instance always fails.
The easiest way to fix the code would be to just use the actual Brushes instances:
private void Timer_Tick(object sender, System.EventArgs e)
{
if (Background == Brushes.Black)
{
Background = Brushes.White;
Title = "White";
}
else
{
Background = Brushes.Black;
Title = "Black";
}
}
Then when you compare the instance references, they are in fact comparable, and you will detect the "black" condition as desired.
I'll note that since you also aren't raising PropertyChanged for changes to the Title property, that binding also will not work as expected.
For what it's worth, I would avoid your design altogether. First, view model objects should avoid using UI-specific types. For sure, this would include the Brush type. Arguably, it also includes the DispatcherTimer, since that exists in service of the UI. Besides which, DispatcherTimer is a relatively imprecise timer, and while it exists primarily to have a timer that raises its Tick event on the dispatcher thread that owns the timer, since WPF automatically marshals property-change events from any other thread to the UI thread, it's not nearly as useful in this example.
Here is a version of your program that IMHO is more in line with typical WPF programming practices:
class MainViewModel : NotifyPropertyChangedBase
{
private string _title;
public string Title
{
get { return _title; }
set { _UpdateField(ref _title, value); }
}
private bool _isBlack;
public bool IsBlack
{
get { return _isBlack; }
set { _UpdateField(ref _isBlack, value, _OnIsBlackChanged); }
}
private void _OnIsBlackChanged(bool obj)
{
Title = IsBlack ? "Black" : "White";
}
public MainViewModel()
{
IsBlack = true;
_ToggleIsBlack(); // fire and forget
}
private async void _ToggleIsBlack()
{
while (true)
{
await Task.Delay(TimeSpan.FromMilliseconds(100));
IsBlack = !IsBlack;
}
}
}
This view model class uses a base class that I use for all my view models, so I don't have to reimplement INotifyPropertyChanged all the time:
class NotifyPropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void _UpdateField<T>(ref T field, T newValue,
Action<T> onChangedCallback = null,
[CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return;
}
T oldValue = field;
field = newValue;
onChangedCallback?.Invoke(oldValue);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
You'll notice that the view model class doesn't have any UI-specific behavior. It would work with any program, WPF or otherwise, as long as that program has a way to react to PropertyChanged events, and to make use of the values in the view model.
To make this work, the XAML gets somewhat more verbose:
<Window x:Class="TestSO55437213TimerBackgroundColor.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:p="http://schemas.microsoft.com/netfx/2007/xaml/presentation"
xmlns:l="clr-namespace:TestSO55437213TimerBackgroundColor"
mc:Ignorable="d"
Title="{Binding Title}" Height="450" Width="800">
<Window.DataContext>
<l:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.Style>
<p:Style TargetType="Grid">
<Setter Property="Background" Value="White"/>
<p:Style.Triggers>
<DataTrigger Binding="{Binding IsBlack}" Value="True">
<Setter Property="Background" Value="Black"/>
</DataTrigger>
</p:Style.Triggers>
</p:Style>
</Grid.Style>
</Grid>
</Window>
(Note: I have explicitly named the http://schemas.microsoft.com/netfx/2007/xaml/presentation XML namespace, for use with the <Style/> element, solely as a work-around for Stack Overflow's insufficient XML markup handling, which would not otherwise recognize the <Style/> element as an actual XML element. In your own program, you may feel free to leave that out.)
The key here is that the entire handling of the UI concern is in the UI declaration itself. The view model doesn't need to know how the UI represents the colors black or white. It just toggles a flag. Then the UI monitors that flag, and applies property setters as appropriate to its current value.
Finally, I'll note that for repeatedly-changing state in the UI like this, another approach is to use WPF's animation features. That's beyond the scope of this answer, but I encourage you to read about it. One advantage to doing so is that the animation uses an even higher-resolution timing model than the thread-pool based Task.Delay() method I use above, and so would generally provide an even smoother animation (though, certainly as your interval gets smaller and smaller — such as 25ms as your post indicates you intended to use — you will have trouble getting WPF to keep up smoothly regardless; at some point, you'll find that the higher-level UI frameworks like WinForms, WPF, Xamarin, etc. just can't operate at such a fine-grained timer level).
I can't seem to figure out why the listbox items are place ontop of each other... they should be below one another. Heres some markup and code.
<ListBox x:Name="DeviceList" Background="#ff4c4c4c" BorderThickness="0" ScrollViewer.CanContentScroll="False" MouseEnter="DeviceList_MouseEnter" MouseLeave="DeviceList_MouseLeave"
ManipulationBoundaryFeedback="DeviceList_ManipulationBoundaryFeedback" ItemContainerStyle="{DynamicResource ResourceKey=ListBoxItemStyle}" PreviewMouseDown="DeviceList_PreviewMouseDown"
PreviewMouseMove="DeviceList_PreviewMouseMove" PreviewMouseUp="DeviceList_PreviewMouseUp" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" DockPanel.Dock="Bottom">
<ListBox.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="..\Utilities\Resources\Themes\Slider.xaml"></ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</ListBox.Resources>
</ListBox>
#region Constructors
/// <summary>
/// Constructor.
/// </summary>
public ConfiguratorView()
{
InitializeComponent();
foreach (Device device in (Application.Current.Windows[1].DataContext as ConfiguratorViewModel).AvailableDevices)
{
devices.Add(AddDevice(device.Icon + "_def", device.Description));
}
DeviceList.ItemsSource = devices;
}
#endregion
#region Internal Members
/// <summary>
/// Add the device to the list of devices.
/// </summary>
/// <param name="icon"></param>
/// <param name="description"></param>
/// <returns></returns>
private Canvas AddDevice(string icon, string description)
{
Canvas canvas = new Canvas();
canvas.Name = icon;
ContentControl backgroundContent = new ContentControl();
Label label = new Label();
backgroundContent.Template = Application.Current.FindResource(icon) as ControlTemplate;
label.Content = description;
canvas.Children.Add(backgroundContent);
canvas.Children.Add(label);
return canvas;
}
The device list adds the canvas as the item... and then i set the ItemsSource to the List. Loading it shows all icons right on top of the last one. Any thoughts?
Everything will appear on top of each other because Canvas.Top defaults to NaN.
You could manually calculate the appropriate Canvas.Top values, but I would suggest:
Keeping the Device object simple with a property for Description and icon
Creating a DataTemplate for the ListBox items to display those properties.
EDIT:
For example (I haven't tested this)
Say your Device class looks something like this:
public class Device
{
public string Description { get; set; }
public object Icon { get; set; }
}
And then your datatemplete for the ListBox could look like this:
<ListBox ItemsSource="{Binding Devices}">
<ListBox.ItemsTemplate>
<DataTemplate>
<Canvas>
<!-- add items here -->
<TextBlock Text="{Binding Description}" Canvas.Top="5" />
<Canvas>
</DataTemplate>
</ListBox.ItemsTemplate>
</ListBox>
I have a TreeView whose ItemsSource is set to a Model I have. 3 levels deep is an object whose state can change and, rather than write a View and ViewModel for each stage (very lengthy business) I wanted to just update this using code.
So basically I have an event that is fired once my model updates, I catch it then find the TreeViewItem associated with this bit of data. My issue now is I have NO idea on how to update the binding on it to reflect the new value!
Can anyone help?
I know this isn't best practice but I really don't want to have to waste time writing a massive amount of code to just update one thing.
Thanks
Chris
Are you sure it wouldn't be easier to implement INotifyPropertyChanged on the relevant class?
This example works, though it's only two (not 3) levels deep. It shows a simple 2-level hierarchical treeview with parent items A, B, and C, with numbered children (A.1, B.1, etc). When the Rename B.1 button is clicked, it renames B.1 to "Sylvia".
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace UpdateVanillaBindingValue
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
private DataClass _data;
public Window1()
{
InitializeComponent();
var data = CreateData();
DataContext = _data = data;
}
private DataClass CreateData()
{
return new DataClass
{
Parents=new List<Parent>
{
new Parent{Name="A",Children=new List<Child>{new Child{Name="A.0"},new Child{Name="A.1"}}},
new Parent{Name="B",Children=new List<Child>{new Child{Name="B.0"},new Child{Name="B.1"},new Child{Name="B.2"}}},
new Parent{Name="C",Children=new List<Child>{new Child{Name="C.0"},new Child{Name="C.1"}}}
}
};
}
private void Rename_Click(object sender, RoutedEventArgs e)
{
var parentB = _data.Parents[1];
var parentBItem = TheTree.ItemContainerGenerator.ContainerFromItem(parentB) as TreeViewItem;
parentB.Children[1].Name = "Sylvia";
var parentBItemsSource = parentBItem.ItemsSource;
parentBItem.ItemsSource = null;
parentBItem.ItemsSource = parentBItemsSource;
}
}
public class DataClass
{
public List<Parent> Parents { get; set; }
}
public class Parent
{
public string Name { get; set; }
public List<Child> Children { get; set; }
}
public class Child
{
public string Name { get; set; }
}
}
<Window x:Class="UpdateVanillaBindingValue.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Grid.Resources>
<DataTemplate x:Key="ChildTemplate">
<TextBlock Margin="50,0,0,0" Text="{Binding Name}" />
</DataTemplate>
<HierarchicalDataTemplate x:Key="ParentTemplate" ItemsSource="{Binding Children}" ItemTemplate="{StaticResource ChildTemplate}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</Grid.Resources>
<TreeView x:Name="TheTree" ItemsSource="{Binding Parents}" ItemTemplate="{StaticResource ParentTemplate}" />
<Button VerticalAlignment="Bottom" HorizontalAlignment="Center" Content="Rename B.1" Click="Rename_Click" />
</Grid>
</Window>
This is a hack, but it re-evaluates the DataTemplate every time it's ItemsSource property changes.
Ideally, you would implement INotifyPropertyChanged on your model object class that this TreeViewItem is bound to, and have it fire the PropertyChanged event when that value changes. In fact, you should be careful that you aren't incurring a memory leak because it doesn't: Finding memory-leaks in WPF Applications.
Sounds like you might need to take a look at the UpdateSource and UpdateTarget methods.
MSDN reference for UpdateSource
Although I'm not totally sure this will work when you've actually bound the TreeView's ItemSource to a hierarchical data structure, but it's where I would start investigating.