I've having trouble understanding a ListBox ItemsSource binding error I'm getting. I followed the details on the Gong WPF.DragDrop GitHub project but I'm getting this error on my binding path:
The project here is what I'm working on and shows the issue. I've extended a little by using a generic type on my observable item so i can make the ListBoxItemViewModel reusable but I found that going without the generic type it still fails.
This is the XAML used for the ListBox.
<ListBox Width="300px" x:Name="clothingItems" Margin="10px" AllowDrop="True"
ItemsSource="{Binding Items}"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True"
dd:DragDrop.DragHandler="{Binding}"/>
This is the code for my ListBoxViewModel.
public class ListBoxViewModel<TItemVm> : IDropTarget
where TItemVm : class, IDropTargetItemViewModel<TItemVm>
{
public ObservableCollection<TItemVm> Items = new ObservableCollection<TItemVm>();
public void DragOver(IDropInfo dropInfo)
{
var sourceItem = dropInfo.Data as TItemVm;
var targetItem = dropInfo.TargetItem as TItemVm;
if (sourceItem != null && targetItem != null && targetItem.CanAcceptChildren)
{
dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight;
dropInfo.Effects = System.Windows.DragDropEffects.Copy;
}
}
public void Drop(IDropInfo dropInfo)
{
var sourceItem = dropInfo.Data as TItemVm;
var targetItem = dropInfo.TargetItem as TItemVm;
targetItem.Children.Add(sourceItem);
}
}
ListBoxViewModel has a public ObservableCollection property called Items and an instance is assigned as the DataContext so why wouldn't ItemsSource="{Binding Items}" resolve?
The issue is that Items in ListBoxViewModel<TItemVm> is a field, not a property.
public ObservableCollection<TItemVm> Items = new ObservableCollection<TItemVm>();
Change the member like below to make it a property and enable binding it.
public ObservableCollection<TItemVm> Items { get; } = new ObservableCollection<TItemVm>();
See Binding Source Types for more information on which binding sources can be used in WPF.
Related
I have a strange use case for WPF DataGrid using MVVM through ReactiveUI that doesn't quite fit any other solution I've found so far.
The Problem Set
I have a DataSet that contains a list of Users. Each User has a string Id and a set of uniquely-identified data fields associated with it that can be represented as a set of string key-value pairs. All Users within a DataSet will have the same set of fields, but different DataSets may have different fields. For example, all Users in one DataSet may have fields "Name", "Age", and "Address"; while Users in another DataSet may have fields "Badge #" and "Job Title".
I would like to present the DataSets in a WPF DataGrid where the columns can be dynamically populated. I would also like to add some metadata to fields that identify what type of data is stored there and display different controls in the DataGrid cells based on that metadata: Pure text fields should use a TextBox, Image filepath fields should have a TextBox to type in a path and a Button to bring up a file-select dialog, etc.
What I Have That Works (but isn't what I want)
I break my data up into ReactiveUI ViewModels. (omitting RaisePropertyChanged() calls for brevity)
public class DataSetViewModel : ReactiveObject
{
public ReactiveList<UserViewModel> Users { get; }
public UserViewModel SelectedUser { get; set; }
};
public class UserViewModel : ReactiveObject
{
public string Id { get; set; }
public ReactiveList<FieldViewModel> Fields { get; }
public class FieldHeader
{
public string Key { get; set; }
public FieldType FType { get; set; } // either Text or Image
}
public ReactiveList<FieldHeader> FieldHeaders { get; }
};
public class FieldViewModel : ReactiveObject
{
public string Value { get; set; } // already knows how to update underlying data when changed
}
I display all of this in a DataSetView. Since Id is always present in Users, I add the first DataGridTextColumn here. Omitting unnecessary XAML for more brevity.
<UserControl x:Class="UserEditor.UI.DataSetView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:UserEditor.UI"
x:Name="DataSetControl">
<DataGrid Name="UserDataGrid"
SelectionMode="Single" AutoGenerateColumns="False"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
DataContext="{Binding Path=ViewModel.Users, ElementName=DataSetControl}">
<DataGrid.Columns>
<DataGridTextColumn Header="Id" Binding="{Binding Id}" MinWidth="60" Width="SizeToCells"/>
</DataGrid.Columns>
</DataGrid>
</UserControl>
And I create additional columns in the code-behind, omitting boiler plate:
public partial class DataSetView : UserControl, IViewFor<DataSetViewModel>
{
// ViewModel DependencyProperty named "ViewModel" declared here
public DataSetView()
{
InitializeComponent();
this.WhenAnyValue(_ => _.ViewModel).BindTo(this, _ => _.DataContext);
this.OneWayBind(ViewModel, vm => vm.Users, v => v.UserDataGrid.ItemsSource);
this.Bind(ViewModel, vm => vm.SelectedUser, v => v.UserDataGrid.SelectedItem);
}
// this gets called when the ViewModel is set, and when I detect fields are added or removed
private void InitHeaders(bool firstInit)
{
// remove all columns except the first, which is reserved for Id
while (UserDataGrid.Columns.Count > 1)
{
UserDataGrid.Columns.RemoveAt(UserDataGrid.Columns.Count - 1);
}
if (ViewModel == null)
return;
// using all DataGridTextColumns for now
for (int i = 0; i < ViewModel.FieldHeaders.Count; i++)
{
DataGridColumn column;
switch (ViewModel.FieldHeaders[i].Type)
{
case DataSet.UserData.Field.FieldType.Text:
column = new DataGridTextColumn
{
Binding = new Binding($"Fields[{i}].Value")
};
break;
case DataSet.UserData.Field.FieldType.Image:
column = new DataGridTextColumn
{
Binding = new Binding($"Fields[{i}].Value")
};
break;
}
column.Header = ViewModel.FieldHeaders[i].Key;
column.Width = firstInit ? DataGridLength.SizeToCells : DataGridLength.SizeToHeader;
UserDataGrid.Columns.Add(column);
}
}
When Fields get added or remove, the UserViewModels are updated in DataSetViewModel and InitHeaders is called to recreate the columns. The resulting DataGridCells bind to their respective FieldViewModels and everything works.
What I'm Trying To Do (but doesn't work)
I would like to break FieldViewModel into two derived classes, TextFieldViewModel and ImageFieldViewModel. Each has their respective TextFieldView and ImageFieldView with their own ViewModel dependency property. UserViewModel still contains a ReactiveList. My new InitHeaders() looks like this:
private void InitHeaders(bool firstInit)
{
// remove all columns except the first, which is reserved for Id
while (UserDataGrid.Columns.Count > 1)
{
UserDataGrid.Columns.RemoveAt(UserDataGrid.Columns.Count - 1);
}
if (ViewModel == null)
return;
for (int i = 0; i < ViewModel.FieldHeaders.Count; i++)
{
DataGridTemplateColumn column = new DataGridTemplateColumn();
DataTemplate dataTemplate = new DataTemplate();
switch (ViewModel.FieldHeaders[i].Type)
{
case DataSet.UserData.Field.FieldType.Text:
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(TextFieldView));
factory.SetBinding(TextFieldView.ViewModelProperty,
new Binding($"Fields[{i}]"));
dataTemplate.VisualTree = factory;
dataTemplate.DataType = typeof(TextFieldViewModel);
}
break;
case DataSet.UserData.Field.FieldType.Image:
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(ImageFieldView));
factory.SetBinding(ImageFieldView.ViewModelProperty,
new Binding($"Fields[{i}]"));
dataTemplate.VisualTree = factory;
dataTemplate.DataType = typeof(ImageFieldViewModel);
}
break;
}
column.Header = ViewModel.FieldHeaders[i].Key;
column.Width = firstInit ? DataGridLength.SizeToCells : DataGridLength.SizeToHeader;
column.CellTemplate = dataTemplate;
UserDataGrid.Columns.Add(column);
}
}
The idea is that I create a DataGridTemplateColumn that generates the correct View and then binds the indexed FieldViewModel to the ViewModel dependency property. I have also tried adding a Converter to the Bindings that converts from the base VM to the correct derived type.
The end result is that the DataGrid populates with the correct view, but the DataContext is always a UserViewModel rather than the appropriate FieldViewModel-derived type. The ViewModel is never set, and the VMs don't bind properly. I'm not sure what else I'm missing, and would appreciate any suggestions or insight.
I've figured out an answer that works, though it may not be the best one. Rather than binding to the ViewModel property in my views, I instead bind directly to the DataContext:
factory.SetBinding(DataContextProperty, new Binding($"Fields[{i}]"));
In my views, I add some boilerplate code to listen to the DataContext, set the ViewModel property, and perform my ReactiveUI binding:
public TextFieldView()
{
InitializeComponent();
this.WhenAnyValue(_ => _.DataContext)
.Where(context => context != null)
.Subscribe(context =>
{
// other binding occurs as a result of setting the ViewModel
ViewModel = context as TextFieldViewModel;
});
}
Having an issue with selection of my treeview.
The idea is that the user clicks the item and data is added under the node that they selected
public MainWindow()
{
InitializeComponent();
this.UserControl1.TreeView1.SelectedItemChanged += new RoutedPropertyChangedEventHandler<Object>(InterfaceTreeViewComputers_SelectionChange);
}
void InterfaceTreeViewComputers_SelectionChange(Object sender, RoutedPropertyChangedEventArgs<object> e)
{
var MyTreeView = MainWindow.UserControl1.Treeview1.Items.SourceCollection;
var TheSource = sender as TreeView;
var TheProperty = e.Source;
var ThePropertyAsTreeView = TheProperty as TreeView
TreeViewItem item = e.OriginalSource as TreeViewItem; //Equals Null
var Attempt2 = ThePropertyAsTreeView.SelectedItem //Equals Null
var Attempt3 = TheSource.SelectedItem as TreeViewItem //Equals Null
var Attempt4 = TheSource.SelectedItem //Equals onbject(String)
}
It seems that the selected item is a textblock and i cant seem to find a way to get it as a treeview item to add nodes under it.
Yes i am pretty new to this type of programming.
Thank you for any help you may provide.
Try this to get selected item:
TreeViewItem item = e.NewValue as TreeViewItem;
Or this to get previously selected item:
TreeViewItem item = e.OldValue as TreeViewItem;
e.Source and e.OriginalSource refer to the TreeView not the TreeViewItem selected. You can use breakpoint then see those properties value and type in Visual Studio's watch. That what I was doing before posting this.
I believe you are approaching this the wrong way. Most of your problems will disappear if you use DataBinding to populate your TreeView. Take into account the following class
namespace StackOverflow._20716616
{
public class Model
{
public Model()
{ Items = new ObservableCollection<Model>(); }
public Model(string text)
: this()
{ Text = text; }
public string Text { get; set; }
public ObservableCollection<Model> Items { get; set; }
}
}
Using this class as the model of an MVVM pattern, I can create a hierarchical object graph which I can bind to the TreeView using ItemsSource and a HierarchicalDataTemplate like
<TreeView ItemsSource="{Binding Path=Items}" SelectedItemChanged="TreeView_OnSelectedItemChanged">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type this:Model}" ItemsSource="{Binding Path=Items}">
<TextBlock Text="{Binding Path=Text}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
The DataContext of the window was set up on the root node of the xaml
DataContext="{Binding RelativeSource={RelativeSource Self}}"
In the code behind, I populate the root items of the TreeView
public MainWindow()
{
Items = new ObservableCollection<Model>() { new Model("one"), new Model("two"), new Model("three") };
InitializeComponent();
}
public ObservableCollection<Model> Items { get; set; }
Then, in the SelectedItemChanged, I can just add the children to the object graph
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var model = e.NewValue as Model;
if (!model.Items.Any())
{
new List<Model>() { new Model("one"), new Model("two"), new Model("three") }
.ForEach(model.Items.Add);
}
// expand the selected item.
var item = ((TreeView)sender).ItemContainerGenerator.ContainerFromItem(model) as TreeViewItem;
item.IsExpanded = true;
}
Note, this is poor man's MVVM where I have used the Windows code behind as the ViewModel. I have also left out null checks and exception catching for the sake of brevity.
I hope this helps
Your right, i am so sorry. http://www.dotnetperls.com/ have given me heaps of insite on this language.
Im too used to programming in things like Visual Basic and Delphi. ive realize now that my MainWindow needs to perform all the pysical actions to my front end user interface and the classes in the other namespaces do not do this.
I have realize that you call a class to perform a specific operation. the class then either returns the information or stores the information in a public property. in which the MainWindow can utilize this and perform the Action required. This i didnt realizeand was trying to manipulate my User Interface from other classes.
I have since restructed my project and the information that was provided here has helped me with my events.
now i have UI coded in XAML
Main Window (Performin events and updating the UI)
Other Classes (Getting data, or designing controls) so the Main Window can add the control or the data to the UI.
I reel like a nub now.
public class Emp
{
public string Id { get; set; }
}
I declared the class like this, but property i didnt set. I set the dependency property for textblock
public static readonly DependencyProperty LabelProperty
= DependencyProperty.Register("TextBlock", typeof(string), typeof(WindowsPhoneControl1), new PropertyMetadata(new PropertyChangedCallback(LabelChanged)));
public string List
{
get { return (string)GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}
This is perhaps not the answer, but there is something fundamentally wrong with your code.
You bind your ListBox's ItemSource property to a property Emp. Then in the Click handler you add objects of type Emp to the Items property of the ListBox. This won't work.
In order to make it work with binding, there has to be a property EmpList of some enumerable type, preferably ObservableCollection. The binding also needs to know the (model) object that defines this property. Therefore you must either set the ListBox's DataContext or you specify the Source of the binding.
When you add elements to the data-bound ListBox, you don't add them to the Items, but instead to the source property of the binding, EmpList here.
public class Model
{
private ICollection<Emp> empList = new ObservableCollection<Emp>();
public ICollection<Emp> EmpList { get { return empList; }}
}
Bind like this:
<ListBox ItemsSource="{Binding EmpList, Source={ an instance of Model }}" ... />
Or like below
<ListBox Name="listBox" ItemsSource="{Binding EmpList}" ... />
and set DataContext, perhaps in code:
listBox.DataContext = model; // where model is an instance of class Model
for (int i = 0; i < result.Length; i++)
{
Emp data = new Emp { Id = result[i] };
model.EmpList.Add(data);
}
Tried may approches to displaying a "no data" if there are no items in listbox. Since I'm on wp7 and using silverlight I can't use DataTriggers, so I've created a control to have it behave consistently across the whole app. BUT I if you set the breakpoint for the set method - it's not being called at all!
The control class
public class EmptyListBox : ListBox
{
public new IEnumerable ItemsSource
{
get
{
return base.ItemsSource;
}
set
{
// never here
base.ItemsSource = value;
ItemsSourceChanged();
}
}
protected virtual void ItemsSourceChanged()
{
bool noItems = Items.Count == 0;
if (noItems)
{
if (Parent is System.Windows.Controls.Panel)
{
var p = Parent as Panel;
TextBlock noData = new TextBlock();
noData.Text = "No data";
noData.HorizontalAlignment = HorizontalAlignment;
noData.Width = Width;
noData.Height = Height;
noData.Margin = Margin;
p.Children.Add(noData);
Visibility = System.Windows.Visibility.Collapsed;
}
}
}
}
This is xaml
<my:EmptyListBox ItemsSource="{Binding Path=MyData}" Name="myListBox">
<my:EmptyListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=name}" />
</DataTemplate>
</my:EmptyListBox.ItemTemplate>
</my:EmptyListBox>
Codebehind:
ClientModel ClientInfo { get; set; }
public ClientView()
{
ClientInfo = new ClientModel();
ClientInfo.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(DataReady);
DataContext = ClientInfo
}
ClientModel class:
public class ClientModel : INotifyPropertyChanged
{
MyData _myData;
public MyData MyData
{
get
{
return _myData;
}
set
{
_myData = value;
NotifyPropertyChanged("MyData");
}
}
public void GetClient(int id)
{
// fetch the network for data
}
}
LINK TO SOLUTION .ZIP THAT SHOWS THE PROBLEM
http://rapidshare.com/files/455900509/WindowsPhoneDataBoundApplication1.zip
Your new ItemSource should be a DependencyProperty.
Anything that is working with Bindings have to be a DependencyProperty.
Simply make it a DependencyProperty.
I think the solution I'd go for is something like this:
Define a new visual state group ItemsStates and two visual states: NoItems and HasItems.
In the ControlTemplate for your custom listbox, add the visual tree for your "no data" state.
In the NoItems state, set the Visibility of your "no data" elements to Visible and set the Visibility of the default ItemsPresenter to Collapsed.
In the HasItems state, swap the Visibility of these elements.
In an OnApplyTemplate override switch to the Empty state by default: VisualStateManager.GoToState(this, "Empty", true);
In an OnItemsChanged override, check whether the items source is empty and use VisualStateManager to switch between these states accordingly.
That should work :)
Create ItemsSource as a DependencyProperty.
Example:
public IEnumerable ItemsSource
{
get { return (IEnumerable)base.GetValue(ItemsSourceProperty); }
set { base.SetValue(ItemsSourceProperty, value); }
}
public static DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
"ItemsSource",
typeof(IEnumerable),
typeof(EmptyListBox),
new PropertyMetadata(null));
try to implement the INotifyPropertyChanged interface and use for ItemsSource an ObservableCollection. In the Setter of your Property just call the OnPropertyChanged method.
Maybe this will help.
Try adding Mode=TwoWay to the ItemsSource binding:
<my:EmptyListBox ItemsSource="{Binding Path=MyData, Mode=TwoWay}" Name="myListBox">
<my:EmptyListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=name}" />
</DataTemplate>
</my:EmptyListBox.ItemTemplate>
</my:EmptyListBox>
I have created an ObservableCollection in the code behind of a user control. It is created when the window loads:
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
Entities db = new Entities();
ObservableCollection<Image> _imageCollection =
new ObservableCollection<Image>();
IEnumerable<library> libraryQuery =
from c in db.ElectricalLibraries
select c;
foreach (ElectricalLibrary c in libraryQuery)
{
Image finalImage = new Image();
finalImage.Width = 80;
BitmapImage logo = new BitmapImage();
logo.BeginInit();
logo.UriSource = new Uri(c.url);
logo.EndInit();
finalImage.Source = logo;
_imageCollection.Add(finalImage);
}
}
I need to get the ObservableCollection of images which are created based on the url saved in a database. But I need a ListView or other ItemsControl to bind to it in XAML file like this:
But I can't figure it out how to pass the ObservableCollection to the ItemsSource of that control. I tried to create a class and then create an instance of a class in xaml file but it did not work. Should I create a static resource somehow>
Any help will be greatly appreciated.
Firstly, the ObservableCollection is a local variable. What you need to do is have it as a private global variable and expose it with a public property. You can use the INotifyPropertyChanged interface to have the image data update automagically when the actual collection itself changes.
In your XAML, you then need to set the DataContext to self, and you can then directly bind your public property to the ItemsSource. You may want to use an ItemTemplate for displaying the items in a custom manner.
Cheers,
Adam
Example as requested:
In C#:
public MyWindowClass
{
public ObservableCollection<image> MyImageCollection
{
get;
set;
}
}
In XAML:
<UserControl
...
DataContext="{Binding RelativeSource={RelativeSource Self}}">
...
<ListBox ItemsSource="{Binding MyImageCollection}" ItemTemplate="*yourtemplateresource*" />
...
</UserControl>
Now, the reason that I mentioned using INotifyPropertyChanged is that if you try:
MyImageCollection = new ObservableCollection<image>();
The items in the listbox will not automatically update. With an ObservableCollection, however, you do not need to implement INotifyPropertyChanged for basic addition and removal of list items.
You have to set the DataContext of the UserControl to your collection:
DataContext = _imageCollection
You can do that in the UserControl_Loaded() method.
Next you need to bind the ItemsSource of the ListView in the XAML:
<ListView ItemsSource="{Binding}"/>
The {Binding} is equivalent to {Binding .} which binds to the DataContext of the UserControl. If you need "more stuff" in your DataContext you can instead create a class like this:
class ViewModel : INotifyPropertyChanged {
public ObservableCollection Images { get { ... } }
...
}
Use this class for the DataContext:
DataContext = new ViewModel();
And replace the binding to bind to the Images property:
<ListView ItemsSource="{Binding Images}"/>
Then you can add another property to ViewModel:
class ViewModel : INotifyPropertyChanged {
public ObservableCollection Images { get { ... } }
public String Message { get { ... } set { ... } }
...
}
And bind it to a control:
<TextBlock Text="{Binding Message}"/>
Remember to fire the PropertyChanged event when the Message property is changed in ViewModel. This will update the UI when view-model properties are changed by code.