I'm making a UserControl to generate a list of attached files from Dependency Property as ItemSource. But the ItemSource (DependencyProperty) count is 0.
I tried debugging and realized that the ObservableCollection in ViewModel was bound after the Constructor of my UserControl is initialized.
I'm coding in MVVM pattern, I made a function to prepare some sample data for ObservableCollection in ViewModel and inside the MainWindow I bound the DataContext of my UserControl with that ViewModel then set the ItemSource for ObservableCollection
My ViewModel code-behind:
//The properties
ObservableCollection<FileAttachmentModel> filesAttachment;
public ObservableCollection<FileAttachmentModel> FilesAttachment
{
get { return filesAttachment; }
set { filesAttachment = value; OnPropertyChanged("FilesAttachment"); }
}
//The function prepare sample data
private ObservableCollection<FileAttachmentModel> PrepareData()
{
FilesAttachment.Add(new FileAttachmentModel() { FileName = "TrackA", FilePath = "D:\trackA.png" });
FilesAttachment.Add(new FileAttachmentModel() { FileName = "TrackB", FilePath = "D:\trackB.png" });
FilesAttachment.Add(new FileAttachmentModel() { FileName = "TrackC", FilePath = "D:\trackC.png" });
}
My UserControl xaml:
<UserControl x:Class="MailSender.Controls.FileAttachment.FileAttachment"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MailSender.Controls.FileAttachment"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Name="fileAttachmentUC"
>
<Grid>
<WrapPanel DataContext="{Binding ElementName=fileAttachmentUC,Path=DataContext,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" x:Name="wrapPanel">
</WrapPanel>
</Grid>
</UserControl>
My UserControl code-behind:
//the property ItemSource
public static readonly DependencyProperty ItemSourceProperty = DependencyProperty.Register("ItemSource", typeof(ObservableCollection<FileAttachmentModel>), typeof(FileAttachment),new UIPropertyMetadata());
//the wrapper property
public ObservableCollection<FileAttachmentModel> ItemSource
{
get { return (ObservableCollection<FileAttachmentModel>)GetValue(ItemSourceProperty); }
set { SetValue(ItemSourceProperty, value);
}
}
//the function to generate each file attachment and add them to the Wrappanel in UserControl
//I call this function inside constructor of UserControl and pass ItemSource as parameter
void GenerateFileItem(ObservableCollection<FileAttachmentModel> lstFileAttachment)
{
if (lstFileAttachment != null && lstFileAttachment.Count>0)
{
foreach (var item in lstFileAttachment)
{
StackPanel sp = new StackPanel() { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
TextBlock tbFileName = new TextBlock() { Text = item.FileName };
Button btFilePath = new Button() { Content = "X", Tag = item.FilePath };
btFilePath.Click += BtFilePath_Click;
sp.Children.Add(tbFileName);
sp.Children.Add(btFilePath);
sp.Style = Application.Current.FindResource("stackFileItem") as Style;
wrapPanel.Children.Add(sp);
}
}
}
In Usage:
<control:FileAttachment DataContext="{StaticResource vmMainWindow}" ItemSource="{Binding FilesAttachment,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
What I expect is to make a container for attached files like Outlook of Microsoft. Please help!
Thanks in advance!
You should call GenerateFileItem whenever the dependency property is set using a PropertyChangedCallback:
public static readonly DependencyProperty ItemSourceProperty = DependencyProperty.Register("ItemSource",
typeof(ObservableCollection<FileAttachmentModel>), typeof(FileAttachment), new PropertyMetadata(new PropertyChangedCallback(OnChanged));
//the wrapper property
public ObservableCollection<FileAttachmentModel> ItemSource
{
get { return (ObservableCollection<FileAttachmentModel>)GetValue(ItemSourceProperty); }
set { SetValue(ItemSourceProperty, value); }
}
private static void OnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FileAttachment fa = (FileAttachment)d;
fa.GenerateFileItem(fa.ItemSource);
}
The ItemSource property cannot be set before the UserControl has been initialized.
What I was facing is the ObservableCollection I generate in ViewModel is initialized after the UserControl is initialized. The #mm8 solution has fixed my problem by waiting for the ObservableCollection in ViewModel is initialized first and then pass it by the property I bound in MainWindow. Then the UserControl will be initialized and get the ObservableCollection that is passed from ViewModel then generate the custom controls inside my "wrapPanel".
Related
I am trying to achieve rather something simple, and I believe that my approach might be wrong. I am creating a datagrid, where the first column has a seperate width from the other ones. I am using AutoGenerateColumns=true, as it simplifies my work. I cannot use pure XAML as I do not know the amount of columns before runtime, and I was not able to connect XAML and AutoGenerateColumns, so it would use the first column's layout, and then generate the rest.
My approaches:
1) Create two data grids next to each other - the issue with that approach is the need to manage 2 seperate datagrids, I saw issues with scrolling and adjusting their sizes, so I decided to change my approach, to keep everyhting within one DataGrid as the data relates to each other.
2) Trying to get the Datagrid object from Code-Behind so I can set the Width property from the ViewModel class, this would break the MVVM model, and also was difficult for my to implement
3) Current approach - using the AutoGeneratingColumn event, I capture the first column and try to bind to its WidthProperties. Unfortunately this does not seem to work, and I do not know why.
This is my Code-Behind file for the XAML containing the DataGrid
private void DG1_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
string headername = e.Column.Header.ToString();
//Cancel the column you don't want to generate
if (headername == "DATE")
{
Binding binding = new Binding("DateColumnWidth");
binding.Source = DataGrid.DataContext; // DataGrid is the name of the DataGrid
binding.Mode = BindingMode.TwoWay;
binding.Path = new PropertyPath("DateColumnWidth");
BindingOperations.SetBinding(e.Column, ColumnDefinition.MinWidthProperty, binding);
BindingOperations.SetBinding(e.Column, ColumnDefinition.MaxWidthProperty, binding);
e.Column.Header = "Test";
}
}
This is my Proprty in the ViewModel. Whilst debugging the binding source, it attaches to the right class and I see all my properties. It also changes the header of the right column.
private int _DateColumnWidth;
public int DateColumnWidth
{
get { return _DateColumnWidth; }
set
{
_DateColumnWidth = value;
RaisePropertyChanged("DateColumnWidth");
}
}
I set the debugger to show me all the data binding tracing information, no problems arise, but the width is not updating.
What am I doing wrong?
I created a mock up based on your code and it worked. Then I looked more closely and realised you have this:
BindingOperations.SetBinding(e.Column, ColumnDefinition.MinWidthProperty, binding);
ColumnDefinition is the wrong object. It should be DataGridColumn:
BindingOperations.SetBinding(e.Column, DataGridColumn.MaxWidthProperty, binding);
Here is my test. The grid's first column is bound to a ColumnWidth property on ViewModel. There is a Slider control below the grid with the same binding. Sliding the slider changes the first column's width.
MainWindow.xaml:
<Window x:Class="SO_59604847_DataGridBoundColumnWidth.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SO_59604847_DataGridBoundColumnWidth"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DataGrid
AutoGenerateColumns="True"
ItemsSource="{Binding GridItems}"
AutoGeneratingColumn="DataGrid_AutoGeneratingColumn">
</DataGrid>
<Slider Grid.Row="1" Minimum="1" Maximum="1000" Value="{Binding ColumnWidth}" />
</Grid>
</Window>
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column.Header.ToString() == "ColumnOne")
{
var binding = new Binding("ColumnWidth");
binding.Source = this.DataContext;
BindingOperations.SetBinding(e.Column, DataGridColumn.MinWidthProperty, binding);
BindingOperations.SetBinding(e.Column, DataGridColumn.MaxWidthProperty, binding);
}
}
}
ViewModel.cs:
public class ViewModel : INotifyPropertyChanged
{
private double _columnWidth;
public double ColumnWidth
{
get { return _columnWidth; }
set { _columnWidth = value; FirePropertyChanged(); }
}
private List<GridItem> _gridItems = new List<GridItem>()
{
new GridItem() { ColumnOne = "1.1", ColumnTwo = "1.2", ColumnThree = "1.3" },
new GridItem() { ColumnOne = "2.1", ColumnTwo = "2.2", ColumnThree = "2.3" },
new GridItem() { ColumnOne = "3.1", ColumnTwo = "3.2", ColumnThree = "3.3" }
};
public List<GridItem> GridItems
{
get { return _gridItems; }
}
private void FirePropertyChanged([CallerMemberName] string caller = "")
{
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class GridItem
{
public string ColumnOne { get; set; }
public string ColumnTwo { get; set; }
public string ColumnThree { get; set; }
}
It does not do two-way binding because that doesn't make sense if you're binding to the Min and Max column widths (the user can't change these - and indeed cannot change the width because the Min and Max widths are set to the same value). If your intention was to bind the width two-ways then you will need to bind to the WidthProperty dependency property and not the Min/MaxWidthProperty DPs (though, you may need a value converter then because Width is a GridLength not a plain number. Say so if that is what you were trying to do and I'll see if I can update the answer accordingly.
I have different groups of controls bound to different categories of ViewModel classes.
The ViewModels are
MainViewModel
VideoViewModel
AudioViewModel
Question
How can I set the DataContext with XAML instead of C#?
1. I tried adding DataContext="{Binding VideoViewModel}" to the ComboBox XAML, but it didn't work and the items came up empty.
2. I also tried grouping all the ComboBoxes of a certain category inside a UserControl with the DataContext:
<UserControl DataContext="{Binding VideoViewModel}">
<!-- ComboBoxes in here -->
</UserControl>
3. Also tried setting the <Window> DataContext to itself DataContext="{Binding RelativeSource={RelativeSource Self}}"
Data Context
I'm currently setting the DataContext this way for the different categories of controls:
public MainWindow()
{
InitializeComponent();
// Main
this.DataContext =
tbxInput.DataContext =
tbxOutput.DataContext =
cboPreset.DataContext =
MainViewModel.vm;
// Video
cboVideo_Codec.DataContext =
cboVideo_Quality.DataContext =
tbxVideo_BitRate.DataContext =
cboVideo_Scale.DataContext =
VideoViewModel.vm;
// Audio
cboAudio_Codec.DataContext =
cboAudio_Quality.DataContext =
tbxAudio_BitRate.DataContext =
tbxAudio_Volume.DataContext =
AudioViewModel.vm;
}
XAML ComboBox
<ComboBox x:Name="cboVideo_Quality"
DataContext="{Binding VideoViewModel}"
ItemsSource="{Binding Video_Quality_Items}"
SelectedItem="{Binding Video_Quality_SelectedItem}"
IsEnabled="{Binding Video_Quality_IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="105"
Height="22"
Margin="0,0,0,0"/>
Video ViewModel Class
public class VideoViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void OnPropertyChanged(string prop)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(prop));
}
}
public VideoViewModel() { }
public static VideoViewModel _vm = new VideoViewModel();
public static VideoViewModel vm
{
get { return _vm; }
set
{
_vm = value;
}
}
// Items Source
private List<string> _Video_Quality_Items = new List<string>()
{
"High",
"Medium",
"Low",
};
public List<string> Video_Quality_Items
{
get { return _Video_Quality_Items; }
set
{
_Video_Quality_Items = value;
OnPropertyChanged("Video_Quality_Items");
}
}
// Selected Item
private string _Video_Quality_SelectedItem { get; set; }
public string Video_Quality_SelectedItem
{
get { return _Video_Quality_SelectedItem; }
set
{
if (_Video_Quality_SelectedItem == value)
{
return;
}
_Video_Quality_SelectedItem = value;
OnPropertyChanged("Video_Quality_SelectedItem");
}
}
// Enabled
private bool _Video_Quality_IsEnabled;
public bool Video_Quality_IsEnabled
{
get { return _Video_Quality_IsEnabled; }
set
{
if (_Video_Quality_IsEnabled == value)
{
return;
}
_Video_Quality_IsEnabled = value;
OnPropertyChanged("Video_Quality_IsEnabled");
}
}
}
You can instantiate an object in xaml:
<Window.DataContext>
<local:MainWindowViewmodel/>
</Window.DataContext>
And you could do that for your usercontrol viewmodels as well.
It's more usual for any child viewmodels to be instantiated in the window viewmodel. Exposed as public properties and the datacontext of a child viewmodel then bound to that property.
I suggest you google viewmodel first and take a look at some samples.
I'm not sure if this is the correct way, but I was able to bind groups of ComboBoxes to different ViewModels.
I created one ViewModel to reference them all.
public class VM: INotifyPropertyChanged
{
...
public static MainViewModel MainView { get; set; } = new MainViewModel ();
public static VideoViewModel VideoView { get; set; } = new VideoViewModel ();
public static AudioViewModel AudioView { get; set; } = new AudioViewModel ();
}
I used Andy's suggestion <local:VM> in MainWindow.xaml.
<Window x:Class="MyProgram.MainWindow"
...
xmlns:local="clr-namespace:MyProgram"
>
<Window.DataContext>
<local:VM/>
</Window.DataContext>
And used a UserControl with DataContext set to VideoView, with ComboBoxes inside.
Instead of a UserControl, can also just use VideoView.Your_Property_Name on each binding.
<UserControl DataContext="{Binding VideoView}">
<StackPanel>
<ComboBox x:Name="cboVideo_Quality"
ItemsSource="{Binding Video_Quality_Items}"
SelectedItem="{Binding Video_Quality_SelectedItem}"
IsEnabled="{Binding Video_Quality_IsEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Width="105"
Height="22"
Margin="0,0,0,0"/>
<!-- Other ComboBoxes with DataContext VideoView in here -->
</StackPanel>
</UserControl>
Then to access one of the properties:
VM.VideoView.Video_Codec_SelectedItem = "x264";
VM.VideoView.Video_Quality_SelectedItem = "High";
VM.AudioView.Audio_Codec_SelectedItem = "AAC";
VM.AudioView.Audio_Quality_SelectedItem = "320k";
Others have obviously provided good answers, however, the underlying miss of your binding is your first set of DataContext = = = = = to the main view model.
Once you the main form's data context to the MAIN view model, every control there-under is expecting ITS STARTING point as the MAIN view model. Since the MAIN view model does not have a public property UNDER IT of the video and audio view models, it cant find them to bind do.
If you remove the "this.DataContext =", then there would be no default data context and each control SHOULD be able to be bound as you intended them.
So change
this.DataContext =
tbxInput.DataContext =
tbxOutput.DataContext =
cboPreset.DataContext =
MainViewModel.vm;
to
tbxInput.DataContext =
tbxOutput.DataContext =
cboPreset.DataContext =
MainViewModel.vm;
Note: You can find the project below on github now. https://github.com/ReasonSharp/MyTestRepo
I'm creating a simple list control with a scrollbar that will display a collection of objects I pass to it. When a user clicks on one item, I want it to become a selected item, and when he clicks it again, I want it to be unselected. I store the selected item in a SelectedLocation property. While debugging, the property is set appropriately. However, if I place this list control (LocationListView) onto a window and bind to SelectedLocation (like SelectedLocation="{Binding MyLocation}") in a control, the binding won't work, and if I try to use this MyLocation in another binding in the same window (i.e. <TextBox Text="{Binding MyLocation.ID}"/>, where ID is a dependency property), that binding won't show anything changing as I select different items in the list.
Minimal example is a bit large, please bear with me:
List control
XAML
<UserControl x:Class="MyListView.LocationListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MyListView"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid x:Name="locationListView">
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="myStackPanel"/>
</ScrollViewer>
</Grid>
</UserControl>
Code behind
using System.Collections;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
namespace MyListView {
public partial class LocationListView : UserControl {
#region Dependency Properties
public IEnumerable Locations {
get { return (IEnumerable)GetValue(LocationsProperty); }
set { SetValue(LocationsProperty, value); }
}
public static readonly DependencyProperty LocationsProperty =
DependencyProperty.Register("Locations", typeof(IEnumerable), typeof(LocationListView), new PropertyMetadata(null, LocationsChanged));
public MyObject SelectedLocation {
get { return (MyObject)GetValue(SelectedLocationProperty); }
set { SetValue(SelectedLocationProperty, value); }
}
public static readonly DependencyProperty SelectedLocationProperty =
DependencyProperty.Register("SelectedLocation", typeof(MyObject), typeof(LocationListView), new PropertyMetadata(null));
#endregion
private static void LocationsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
((LocationListView)o).RegenerateLocations();
if (((LocationListView)o).Locations is ObservableCollection<MyObject>) {
var l = ((LocationListView)o).Locations as ObservableCollection<MyObject>;
l.CollectionChanged += ((LocationListView)o).L_CollectionChanged;
}
}
private void L_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
RegenerateLocations();
}
private Button selectedLV = null;
public LocationListView() {
InitializeComponent();
}
private void RegenerateLocations() {
if (Locations != null) {
myStackPanel.Children.Clear();
foreach (var l in Locations) {
var b = new Button();
b.Content = l;
b.Click += B_Click;
myStackPanel.Children.Add(b);
}
}
selectedLV = null;
}
private void B_Click(object sender, RoutedEventArgs e) {
var lv = (sender as Button)?.Content as MyObject;
if (selectedLV != null) {
lv.IsSelected = false;
if ((selectedLV.Content as MyObject) == SelectedLocation) {
SelectedLocation = null;
selectedLV = null;
}
}
if (lv != null) {
SelectedLocation = lv;
selectedLV = sender as Button;
lv.IsSelected = true;
}
}
}
}
Note the absence of this.DataContext = this; line. If I use it, I get the following binding expression path errors:
System.Windows.Data Error: 40 : BindingExpression path error: 'SillyStuff' property not found on 'object' ''LocationListView' (Name='')'. BindingExpression:Path=SillyStuff; DataItem='LocationListView' (Name=''); target element is 'LocationListView' (Name=''); target property is 'Locations' (type 'IEnumerable')
System.Windows.Data Error: 40 : BindingExpression path error: 'MySelectedLocation' property not found on 'object' ''LocationListView' (Name='')'. BindingExpression:Path=MySelectedLocation; DataItem='LocationListView' (Name=''); target element is 'LocationListView' (Name=''); target property is 'SelectedLocation' (type 'MyObject')
Using (this.Content as FrameworkElement).DataContext = this; won't produce these errors, but it won't work either.
Main window
XAML
<Window x:Class="MyListView.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:MyListView"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<DockPanel LastChildFill="True" HorizontalAlignment="Stretch" VerticalAlignment="Top">
<local:LocationListView Locations="{Binding SillyStuff}" SelectedLocation="{Binding MySelectedLocation}" DockPanel.Dock="Top"/>
<TextBox Text="{Binding MySelectedLocation.ID}" DockPanel.Dock="Top"/>
</DockPanel>
</Grid>
</Window>
Code behind
using System.Windows;
using Microsoft.Practices.Unity;
namespace MyListView {
public partial class MainWindow : Window {
private MainViewModel vm;
public MainWindow() {
InitializeComponent();
}
[Dependency] // Unity
internal MainViewModel VM {
set {
this.vm = value;
this.DataContext = vm;
}
}
}
}
MainViewModel
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace MyListView {
class MainViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e) {
if (PropertyChanged != null)
PropertyChanged(sender, e);
}
private MyObject mySelectedLocation;
public MyObject MySelectedLocation {
get { return mySelectedLocation; }
set {
mySelectedLocation = value;
OnPropertyChanged(this, new PropertyChangedEventArgs("MySelectedLocation"));
}
}
public ObservableCollection<MyObject> SillyStuff {
get; set;
}
public MainViewModel() {
var cvm1 = new MyObject();
cvm1.ID = 12345;
var cvm2 = new MyObject();
cvm2.ID = 54321;
var cvm3 = new MyObject();
cvm3.ID = 15243;
SillyStuff = new ObservableCollection<MyObject>();
SillyStuff.Add(cvm1);
SillyStuff.Add(cvm2);
SillyStuff.Add(cvm3);
}
}
}
MyObject
using System.Windows;
namespace MyListView {
public class MyObject : DependencyObject {
public int ID {
get { return (int)GetValue(IDProperty); }
set { SetValue(IDProperty, value); }
}
public static readonly DependencyProperty IDProperty =
DependencyProperty.Register("ID", typeof(int), typeof(MyObject), new PropertyMetadata(0));
public bool IsSelected {
get; set;
}
public override string ToString() {
return ID.ToString();
}
}
}
App.xaml -- just to save anyone the typing
XAML
<Application x:Class="MyListView.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyListView">
<Application.Resources>
</Application.Resources>
</Application>
Code behind
using System.Windows;
using Microsoft.Practices.Unity;
namespace MyListView {
public partial class App : Application {
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
UnityContainer container = new UnityContainer();
var mainView = container.Resolve<MainWindow>();
container.Dispose();
mainView.Show();
}
}
}
The objective here is to have the value in the TextBox on MainWindow change to the selected item's ID whenever the selected item changes. I could probably do it by creating a SelectedItemChanged event on my LocationListView, and then setting the property manually in a handler, but that seems like a hack. If you place a <ListView ItemsSource="{Binding SillyStuff}" SelectedItem="{Binding MySelectedLocation}" DockPanel.Dock="Top"/> instead of my list control, this works like a charm, so I should be able to make my control work that way too.
Edit: Changed MainViewModel to implement INotifyPropertyChanged as per Pieter's instructions.
Main issues
When you select an item in your custom control, B_Click assigns it to the SelectedLocation property, which calls SetValue internally. However, this overwrites the binding on SelectedLocation - in other words, after that call SelectedLocation is no longer bound to anything. Use SetCurrentValue instead to preserve the binding.
However, bindings won't update their source by default. You'll have to set their Mode to TwoWay. You can do that in XAML: SelectedLocation="{Binding MySelectedLocation, Mode=TwoWay}", or mark the dependency property to use TwoWay binding by default: new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, LocationsChanged).
Finally, make sure that your binding paths are correct. Your text box binds to SelectedLocation, while the property is named MySelectedLocation. These kind of issues are usually logged in the debug output, in this case you should get a message like this:
System.Windows.Data Error: 40 : BindingExpression path error: 'SelectedLocation' property not found on 'object' ''MainViewModel' (HashCode=8757408)'. BindingExpression:Path=SelectedLocation.ID; DataItem='MainViewModel' (HashCode=8757408); target element is 'TextBox' (Name=''); target property is 'Text' (type 'String')
Other issues
I've found a few other issues as well: you're not unregistering L_CollectionChanged when another collection is set, and if the collection is removed, you're not clearing the visible items. The code in B_Click is also troublesome: you're also accessing lv before making sure it's not null, and if the user clicks on an unselected button you're setting SelectedLocation to null before setting it to the newly selected item. Also, when regenerating items, selectedLV (what's 'lv'?) is set to null, but SelectedLocation is left intact...
Also a little tip: your OnPropertyChanged method only needs a single argument: string propertyName. Make it optional and mark it with a [CallerMemberName] attribute, so all that a property setter needs to do is call it without arguments. The compiler will insert the calling property name for you.
Alternatives
Personally, I'd just use a ListView with a custom ItemTemplate:
<ListView ItemsSource="{Binding MyLocations}" SelectedItem="{Binding MySelectedLocation}" SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<ToggleButton IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}}" Content="{Binding}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
This probably requires a few more modifications to make it look nice, but that's the gist of it. Alternately, you could create an attached behavior that takes care of your desired selection behavior.
Oh boy, that's a lot of code.
Let me begin by highlighting a common mistake, which is setting the control's DataContext to itself. This should be avoided as it tends to screw up absolutely everything.
So. Avoid doing this:
this.DataContext = this;
It is not the responsibility of the UserControl itself to set it's own DataContext, it should be the responsibility of the parent control (such as a Window to set it. Like this:
<Window ...>
<local:MyUserControl DataContext="{Binding SomeProperty}" ... />
If your UserControl was to set its own DataContext, then it will override what the Window sets its DataContext to be. Which will result in the screwing up of absolutely everything.
To bind to a Dependency Property of a UserControl, simply give your control an x:Name and use an ElementName binding, like this:
<UserControl ...
x:Name="usr">
<TextBlock Text="{Binding SomeDependencyProperty, ElementName=usr}" ... />
What's important to note here is that the DataContext isn't being set at all, so your parent Window is free to set the control's DataContext to whatever it needs to be.
Adding to this, your UserControl can now bind to it's DataContext using a straightforward Path binding.
<UserControl ...
x:Name="usr">
<TextBlock Text="{Binding SomeDataContextProperty}" ... />
I hope this helps.
I'm trying to create my own, very simple, Usercontrol in WPF. It's basically just a Combobox, with some additional Logic in it.
I tried to create my own Depdency-Property using this Tutorial: http://www.codeproject.com/Articles/140620/WPF-Tutorial-Dependency-Property
This is working fine so far, but if the property changes, I'd like to reflect this on the Combobox in the User-Control as well. It seems like I can't bind the subcontrol directly to my new Dependency-Project.
My code is looking like this at the moment:
public partial class ClassSelector : UserControl
{
public static readonly DependencyProperty CurrentValueProperty =
DependencyProperty.Register("CurrentValue", typeof(ClassType),
typeof(ClassSelector), new FrameworkPropertyMetadata());
public ClassType CurrentValue
{
get
{
return (ClassType)this.GetValue(CurrentValueProperty);
}
set
{
this.SetValue(CurrentValueProperty, value);
}
}
public ClassSelector()
{
this.DataContext = this;
InitializeComponent();
cmbClassType.ItemsSource = Enum.GetValues(typeof(ClassType));
}
}
Setting the value of the dependy-property or the Combobox seems weirds to me.
I tried to bind it direclty in the xaml via:
<Grid>
<ComboBox x:Name="cmbClassType" SelectedItem="{Binding Path=CurrentValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectionChanged="cmbClassType_SelectionChanged" />
</Grid>
I tried to map the Dependicy-Project changed Event with the combobox and visa versa, but this leads to very strange code, since the combobox change would have to change the property-value and the property-value the combobox.
I'm quite sure there has to be a possibility to bind a DependencyProperty to a subcontrol, but I can't find a way to make this work.
Thanks in advance for all advices guys and have a nice weekend
Matthias
Edith says: The calling Window needs to bind the Object to the Grid, not to the Window, so for example:
grdMain.DataContext = new DeckSearch();
is working fine, meanwhile
this.DataContext = new DeckSearch();
This behavior is ONLY at my custom control, all other controls worked perfectly fine with the DataContext on the Window itself.
Okay so here I fixed your code and it is working at my end
UserControlCodeBehind
public partial class ClassSelector : UserControl
{
public static readonly DependencyProperty CurrentValueProperty = DependencyProperty.Register("CurrentValue", typeof(ClassType),
typeof(ClassSelector), new FrameworkPropertyMetadata()
{
DefaultValue = ClassType.Type1,
BindsTwoWayByDefault = true,
PropertyChangedCallback = CurrentValueChanged,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
private static void CurrentValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var obj = (ClassSelector)d;
obj.cmbClassType.SelectedValue = e.NewValue;
}
public ClassType CurrentValue
{
get
{
return (ClassType)this.GetValue(CurrentValueProperty);
}
set
{
this.SetValue(CurrentValueProperty, value);
}
}
public ClassSelector()
{
InitializeComponent();
cmbClassType.ItemsSource = Enum.GetValues(typeof(ClassType));
cmbClassType.SelectedValue = CurrentValue;
}
}
The Xaml part of the UserControl
<Grid>
<ComboBox x:Name="cmbClassType" SelectedValue="{Binding Path=CurrentValue}"/>
</Grid>
Please check if it is working at your end. I have not added any extra code checking for thread safety and all.
EDIT
In my solution I do get my Class Property notification when the CurrentValue changes.
Below is my sample MainWindow Code.
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
Task.Run(() =>
{
Thread.Sleep(1000);
Dispatcher.InvokeAsync(() =>
{
customCombobox.CurrentValue = ClassType.Type3;//Updating the UserControl DP
});
Thread.Sleep(2000);
this.CurrentValue = ClassType.Type2;//Updating my local Property
});
}
private ClassType _currentValue;
public ClassType CurrentValue
{
get { return _currentValue; }
set
{
_currentValue = value;
Debug.WriteLine("Value Changed to " + value.ToString());
RaisePropertyChanged("CurrentValue");
}
}
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string propName)
{
var pc = PropertyChanged;
if (pc != null)
pc(this, new PropertyChangedEventArgs(propName));
}
}
And my MainWindow.xaml
<Window x:Class="WpfApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication"
Title="MainWindow" Height="250" Width="525">
<local:ClassSelector x:Name="customCombobox" Height="25" CurrentValue="{Binding CurrentValue}"/>
</Window>
I can not understand why the ListView is not refreshing when the ObservableCollection is replaced (with new one) and not changed (added or removed items).
I respected all requirements for property notifications, since I'm using a DependencyObject for my view model, and SetValue is called when the collection is replaced .
I have a WPF ListView bound to a Col property of my view model:
public class ViewModel1 : DependencyObject
{
public ViewModel1()
{
Col = new ObservableCollection<string>(new[] { "A", "B", "C", "D" });
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
Debug.WriteLine("Property changed "+ e.Property.Name);
}
public ObservableCollection<string> Col
{
get { return (ObservableCollection<string>)GetValue(ColProperty); }
set { SetValue(ColProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColProperty =
DependencyProperty.Register("ColProperty", typeof(ObservableCollection<string>), typeof(ViewModel1), new PropertyMetadata(null));
}
The XAML is like:
<Window x:Class="BindingPOC.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel>
<ListView Margin="0,10,0,0" ItemsSource="{Binding Col}" />
<Button Click="Button_Click" >click</Button>
</StackPanel>
</Grid>
</Window>
So with this code, everything work fine if I don't replace the initial ObservableCollection.
But when I click on the button. I replace the list with:
private void Button_Click(object sender, RoutedEventArgs e)
{
(DataContext as ViewModel1).Col = new System.Collections.ObjectModel.ObservableCollection<string>(new[] { "Z", "ZZ" });
}
The PropertyChanged method on the view model is called for Col, but the ListView is not updating its content.
Do I need to conserve the same ObservableCollection reference ? why ?
This is because your dependency property registration is incorrect. The name of the property passed to the Register method should be "Col", not "ColProperty":
public static readonly DependencyProperty ColProperty =
DependencyProperty.Register("Col", typeof(ObservableCollection<string>), typeof(ViewModel1), new PropertyMetadata(null));
The initial binding works because there is a property named Col, but it is not detected as a dependency property, so the binding is not automatically updated.