I used Drag and Drop to bind Data Source object (a DB model) to DataGrid (basically following this example in Entity Framework Databinding with WPF.
Everything works fine with this implementation.
XAML
<Window.Resources>
<CollectionViewSource x:Key="categoryViewSource"
d:DesignSource="{d:DesignInstance {x:Type local:Category}, CreateList=True}"/>
</Window.Resources>
<Grid DataContext="{StaticResource categoryViewSource}">
..
Code Behind
private void Window_Loaded(object sender, RoutedEventArgs e)
{
System.Windows.Data.CollectionViewSource categoryViewSource =
((System.Windows.Data.CollectionViewSource)(this.FindResource("categoryViewSource")));
_context.Categories.Load();
categoryViewSource.Source = _context.Categories.Local;
}
ViewModel
public MainWindow()
{
InitializeComponent();
this.DataContext = new MyViewModel();
}
However, when I try to use the same code from within ViewModel, it doesn‘t work (FindResource is not available), besides, I don’t think this is the right approach (i.e. to use x:Key in MVVM).
I would really appreciate any help to point me what is the right way to implement CollectionViewSource and DataBinding with DataGrid.
You have two options to use CollectionViewSource properly with MVVM -
Expose an ObservableCollection of items (Categories in your case) through your ViewModel and create CollectionViewSource in XAML like this -
<CollectionViewSource Source="{Binding Path=Categories}">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="CategoryName" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
scm: xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
see this - Filtering collections from XAML using CollectionViewSource
Create and Expose an ICollectionView directly from your ViewModel
see this - How to Navigate, Group, Sort and Filter Data in WPF
Following example shows how to create a collection view and
bind it to a ListBox
View XAML:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
x:Class="CustomerView">
<ListBox ItemsSource={Binding Customers} />
</Window>
View Codebehind:
public class CustomerView : Window
{
public CustomerView()
{
DataContext = new CustomerViewModel();
}
}
ViewModel:
public class CustomerViewModel
{
private readonly ICollectionView customerView;
public ICollectionView Customers
{
get { return customerView; }
}
public CustomerViewModel()
{
IList<Customer> customers = GetCustomers();
customerView = CollectionViewSource.GetDefaultView( customers );
}
}
Update:
Q. If there is no property to sort on? e.g. if there is an ObservableCollection of string or int?
A. In that case you can Simply use . as the property name:
<scm:SortDescription PropertyName="." />
I found that it is handy to have a CollectionViewSource in my ViewModel and bind the ListBox (in my case) to the CollectionViewSource.View while setting the CollectionViewSource.Source to be the list I want to use.
Like so:
ViewModel:
public DesignTimeVM() //I'm using this as a Design Time VM
{
Items = new List<Foo>();
Items.Add(new Foo() { FooProp= "1", FooPrep= 20.0 });
Items.Add(new Foo() { FooProp= "2", FooPrep= 30.0 });
FooViewSource = new CollectionViewSource();
FooViewSource.Source = Items;
SelectedFoo = Items.First();
//More code as needed
}
XAML:
<ListBox ItemsSource="{Binding FooViewSource.View}" SelectedItem="{Binding SelectedFoo}"/>
This means I can do neat stuff in the VM as needed (from https://blogs.msdn.microsoft.com/matt/2008/08/28/collectionview-deferrefresh-my-new-best-friend/ ):
using (FooViewSource.DeferRefresh())
{
//Remove an old Item
//add New Item
//sort list anew, etc.
}
I suppose this is possible when using the ICollectionView object also, but the demo code in the blog link seems to be some codebehind stuff, refering the listbox directly, which I'm trying to avoid.
BTW before you ask, here's how you use a Design Time VM: WPF Design Time View Model
Just for reference, another way is to use an attached property on the CollectionViewSource which then pipes the functions to the ViewModel (Implementing an Interface).
This is a very basic Demonstration just for filtering, it would need some work for e.g. a second Collection on the VM but i think it's enough to show the general technique.
If this is better or worse than the other methods is up for discussion, i just wanted to point out, that there's another way of doing this
Definition of attached Property:
public static class CollectionViewSourceFilter
{
public static IFilterCollectionViewSource GetFilterObject(CollectionViewSource obj)
{
return (IFilterCollectionViewSource)obj.GetValue(FilterObjectProperty);
}
public static void SetFilterObject(CollectionViewSource obj, IFilterCollectionViewSource value)
{
obj.SetValue(FilterObjectProperty, value);
}
public static void FilterObjectChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is IFilterCollectionViewSource oldFilterObject
&& sender is CollectionViewSource oldCvs)
{
oldCvs.Filter -= oldFilterObject.Filter;
oldFilterObject.FilterRefresh -= (s, e2) => oldCvs.View.Refresh();
}
if (e.NewValue is IFilterCollectionViewSource filterObject
&& sender is CollectionViewSource cvs)
{
cvs.Filter += filterObject.Filter;
filterObject.FilterRefresh += (s,e2) => cvs.View.Refresh();
}
}
public static readonly DependencyProperty FilterObjectProperty = DependencyProperty.RegisterAttached(
"FilterObject",
typeof(Interfaces.IFilterCollectionViewSource),
typeof(CollectionViewSourceFilter),
new PropertyMetadata(null,FilterObjectChanged)
);
}
Interface:
public interface IFilterCollectionViewSource
{
void Filter(object sender, FilterEventArgs e);
event EventHandler FilterRefresh;
}
usage in xaml:
<CollectionViewSource
x:Key="yourKey"
Source="{Binding YourCollection}"
classes:CollectionViewSourceFilter.FilterObject="{Binding}" />
and usage in the ViewModel:
class YourViewModel : IFilterCollectionViewSource
{
public event EventHandler FilterRefresh;
private string _SearchTerm = string.Empty;
public string SearchTerm
{
get { return _SearchTerm; }
set {
SetProperty(ref _SearchTerm, value);
FilterRefresh?.Invoke(this, null);
}
}
private ObservableCollection<YourItemType> _YourCollection = new ObservableCollection<YourItemType>();
public ObservableCollection<YourItemType> YourCollection
{
get { return _YourCollection; }
set { SetProperty(ref _YourCollection, value); }
}
public void Filter(object sender, FilterEventArgs e)
{
e.Accepted = (e.Item as YourItemType)?.YourProperty?.ToLower().Contains(SearchTerm.ToLower()) ?? true;
}
}
Related
I'm having trouble setting up an automatic binding.
I have a simple WPF application with two classes - MovieInfo (contains information about movie files on my filesystem) and a MediaScanner class that just returns a List<MovieInfo>. In my MainWindow.xaml, I have a ListBox:
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="242" Margin="10,35,0,0" VerticalAlignment="Top" Width="237" d:ItemsSource="{Binding MovieList}"/>
Also in the XAML added to the <Window ...> is
Name="MainWindow1"
DataContext="{Binding ElementName=MainWindow1}"
In the code behind, I made a public property of the MainWindow : Window:
public ObservableCollection<MovieInfo> MovieList { get; set; }
In the constructor:
public MainWindow()
{
DataContext = this;
MovieList = new ObservableCollection<MovieInfo>();
InitializeComponent();
//this doesn't do anything for me
//listBox.ItemsSource = MovieList;
}
I have a button that calls:
private void button_Click(object sender, RoutedEventArgs e)
{
var scanner = new MediaScanner();
MovieList = new ObservableCollection<MovieInfo>(scanner.ScanAll().OrderBy(x => x.Title));
//listBox.ItemsSource = MovieList;
}
It's my understanding that this should take care of everything, yet the ListBox won't populate unless I uncomment the listBox.ItemsSource = MovieList; where it is in the button_Click.
What am I missing?
The ListBox does not bind the MovieList at runtime because you prefixed ItemsSource with d.
After adding the namespaces, you can put d: in front of any attribute or control to show it only in the XAML Designer but not at runtime. [...]
You can use d: with attributes for any UWP or WPF .NET Core control, like colors, font sizes, and spacing. You can even add it to the control itself.
These namespaces are defined on your XAML root element.
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
In order to make the binding work at runtime, remove the d: or add another ItemsSource without the d: prefix so that you have different sources for design and runtime.
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="242" Margin="10,35,0,0" VerticalAlignment="Top" Width="237" ItemsSource="{Binding MovieList}"/>
<ListBox x:Name="listBox" HorizontalAlignment="Left" Height="242" Margin="10,35,0,0" VerticalAlignment="Top" Width="237" d:ItemsSource="{Binding DesignTimeMovieList}" ItemsSource="{Binding MovieList}"/>
Another issue is that you neither implement a dependency property for your movie list collection nor INotifyPropertyChanged. In practice this means although you assign a new collection to the MovieList property in your button click event handler, the binding does not get notified of the change and will still use the old collection instance. Of course listBox.ItemsSource = MovieList; would work here, but it assigns the collection directly and overwrites the binding, so this is a different solution.
In the long run, you should probably apply the MVVM pattern and separate the data to be bound in a view model that implements INotifyPropertyChanged, which solves your issue and at the same time separates your view and your logic and data that are then connected via bindings.
Example of a dependency property for your window.
public partial class MainWindow : Window
{
public ObservableCollection<MovieInfo> MovieList
{
get => (ObservableCollection<MovieInfo>)GetValue(MovieListProperty);
set => SetValue(MovieListProperty, value);
}
public static readonly DependencyProperty MovieListProperty = DependencyProperty.Register(
nameof(MovieList), typeof(ObservableCollection<MovieInfo>), typeof(MainWindow), new PropertyMetadata(null));
public MainWindow()
{
DataContext = this;
MovieList = new ObservableCollection<MovieInfo>();
InitializeComponent();
// ...other code.
}
private void button_Click(object sender, RoutedEventArgs e)
{
var scanner = new MediaScanner();
MovieList = new ObservableCollection<MovieInfo>(scanner.ScanAll().OrderBy(x => x.Title));
}
// ...other code.
}
Example for a view model in case you want to move to MVVM.
public class MyViewModel : INotifyPropertyChanged
{
private ObservableCollection<MovieInfo> _movieList;
public ObservableCollection<MovieInfo> MovieList
{
get => _movieList;
set
{
if (_movieList == value)
return;
_movieList = value;
OnPropertyChanged();
}
}
// ...other code.
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
As a final remark, you should consider reusing the ObservableCollection<MovieInfo> MovieList instead of creating and assigning a new one each time. This type of collection provides change notifications for adding, removing and replacing items, which will automatically be reflected in the user interface. Exposing an observable collection, but instead of modifying, replacing it, is pointless. If a collection is always replaced, a simple List<T> will do the same.
I'm trying to bind a grouped collection of data items to a DataGrid. The details of the presented data are not relevant, in fact all the contents are set up with dummy data for now.
I followed the sample code found in Microsoft's Sample App and "How to: Group, sort and filter data in the DataGrid Control".
After launching the app the shown DataGrid is empty and the debug output from the binding code says:
Error: Converter failed to convert value of type 'Windows.UI.Xaml.Data.ICollectionView' to type 'IBindableIterable'; BindingExpression: Path='MyContents' DataItem='MyViewModel'; target element is 'Microsoft.Toolkit.Uwp.UI.Controls.DataGrid' (Name='null'); target property is 'ItemsSource' (type 'IBindableIterable').
This is the interesting part of my XAML:
<mstkcontrols:DataGrid ItemsSource="{Binding MyContents}">
<!-- Irrelevant stuff left out... -->
</mstkcontrols:DataGrid>
In my view model I have this code:
public ICollectionView MyContents { get; private set; }
public override void OnNavigatedTo(NavigationEventArgs e)
{
// Irrelevant stuff left out...
ObservableCollection<ObservableCollection<MyItemType>> groupedCollection = new ObservableCollection<ObservableCollection<MyItemType>>();
// It doesn't matter how this grouped collection is filled...
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.IsSourceGrouped = true;
collectionViewSource.Source = groupedCollection;
MyContents = collectionViewSource.View;
}
Is there a conversion from ICollectionView to IBindableIterable? If so, how is it done?
I'm well aware that the examples do the binding in the code, not in the XAML. Does this really make a difference?
If this approach is wrong, how is the correct approach?
Edit:
I'm sorry, I forgot to mention that we use the "MVVM Light Toolkit" by GalaSoft. That's why the code to build the collection is in the view model, not the code behind. And it should stay there.
This has an impact on the kind of binding. To bind to a property of the view model, we use:
<mstkcontrols:DataGrid ItemsSource="{Binding MyContents}">
But to bind to a property of the code behind, is has to be:
<mstkcontrols:DataGrid ItemsSource="{x:Bind MyContents}">
In the meantime, thank you very much to all reading and making suggestions. I'm currently investigating how to connect view model and code behind.
Alright, it took me a 2-digit number of hours to find the root of this problem. There seems to be a disrupted way with Binding compared to x:Bind.
"{Binding} assumes, by default, that you're binding to the DataContext of your markup page." says the documentation "Data binding in depth". And the data context of my page is the view model.
"{x:Bind} does not use the DataContext as a default source—instead, it uses the page or user control itself." says the documentation "{x:Bind} markup extension". Well, and the compile-time generated code has no problems with the different data types.
The XAML is changed to (the Mode is important, because the default is OneTime):
<mstkcontrols:DataGrid ItemsSource="{x:Bind MyContents, Mode=OneWay}" Loaded="DataGrid_Loaded">
<!-- Irrelevant stuff left out... -->
</mstkcontrols:DataGrid>
The code behind needs a property that sends notification events. For this its class needs to inherit from INotifyPropertyChanged. You could use the methods Set() and OnPropertyChanged() shown in #NicoZhu's answer, but this cut-out shows more clearly what is important:
private ICollectionView _myContents;
public ICollectionView MyContents
{
get
{
return _myContents;
}
set
{
if (_myContents != value)
{
_myContents = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyContents)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
if ((sender as DataGrid).DataContext is MyViewModel viewModel)
{
MyContents = viewModel.ContentsView();
}
}
The view model provides the contents view (as a collection of collections) through a method that is called from the code behind. This method is almost identical to the code I used before.
internal ICollectionView ContentsView()
{
ObservableCollection<ObservableCollection<MyItemType>> groupedCollection = new ObservableCollection<ObservableCollection<MyItemType>>();
// It doesn't matter how this grouped collection is filled...
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.IsSourceGrouped = true;
collectionViewSource.Source = groupedCollection;
return collectionViewSource.View;
}
I follow this tutorial creating a simple sample to reproduce your issue, And binding CollectionViewSource works well. Please refer the following code. This is sample project.
Xaml
<controls:DataGrid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AlternatingRowBackground="Transparent"
AlternatingRowForeground="Gray"
AreRowDetailsFrozen="False"
AreRowGroupHeadersFrozen="True"
AutoGenerateColumns="False"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
CanUserSortColumns="False"
ColumnHeaderHeight="32"
FrozenColumnCount="0"
GridLinesVisibility="None"
HeadersVisibility="Column"
HorizontalScrollBarVisibility="Visible"
IsReadOnly="False"
ItemsSource="{x:Bind GroupView, Mode=TwoWay}"
Loaded="DataGrid_Loaded"
MaxColumnWidth="400"
RowDetailsVisibilityMode="Collapsed"
RowGroupHeaderPropertyNameAlternative="Range"
SelectionMode="Extended"
VerticalScrollBarVisibility="Visible"
>
<controls:DataGrid.RowGroupHeaderStyles>
<Style TargetType="controls:DataGridRowGroupHeader">
<Setter Property="Background" Value="LightGray" />
</Style>
</controls:DataGrid.RowGroupHeaderStyles>
<controls:DataGrid.Columns>
<controls:DataGridTextColumn
Binding="{Binding Name}"
Header="Rank"
Tag="Rank"
/>
<controls:DataGridComboBoxColumn
Binding="{Binding Complete}"
Header="Mountain"
Tag="Mountain"
/>
</controls:DataGrid.Columns>
</controls:DataGrid>
Code Behind
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
public ObservableCollection<Item> MyClasses { get; set; } = new ObservableCollection<Item>();
private ICollectionView _groupView;
public ICollectionView GroupView
{
get
{
return _groupView;
}
set
{
Set(ref _groupView, value);
}
}
public MainPage()
{
this.InitializeComponent();
MyClasses.Add(new Item { Name = "Nico", Complete = false });
MyClasses.Add(new Item { Name = "LIU", Complete = true });
MyClasses.Add(new Item { Name = "He", Complete = true });
MyClasses.Add(new Item { Name = "Wei", Complete = false });
MyClasses.Add(new Item { Name = "Dong", Complete = true });
MyClasses.Add(new Item { Name = "Ming", Complete = false });
}
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
var groups = from c in MyClasses
group c by c.Complete;
var cvs = new CollectionViewSource();
cvs.Source = groups;
cvs.IsSourceGrouped = true;
var datagrid = sender as DataGrid;
GroupView = cvs.View;
}
public event PropertyChangedEventHandler PropertyChanged;
private void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (Equals(storage, value))
{
return;
}
storage = value;
OnPropertyChanged(propertyName);
}
private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
I don't know how transitive WPF C# is to UWP, but this is how I do my observable collection data binding in WPF
In my window's .cs:
public partial class MainWindowView : Window, INotifyPropertyChanged
{
public MainWindowView()
{
InitializeComponent();
this.data.ItemsSource = etc;
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Stuff_NThings> etc = new ObservableCollection<Stuff_NThings>();
private void Button_Click(object sender, RoutedEventArgs e)
{
Stuff_NThings t = new Stuff_NThings();
t.stuff = 45;
t.moreStuff = 44;
t.things = 33;
t.moreThings = 89;
etc.Add(t);
}
My class:
public class Stuff_NThings : INotifyPropertyChanged
{
private int _things;
private int _moreThings;
private int _stuff;
private int _moreStuff;
public int things
{
get
{
return _things;
}
set
{
_things = value;
NotifyPropertyChanged(nameof(things));
}
}
public int moreThings
{
get
{
return _moreThings;
}
set
{
_moreThings = value;
NotifyPropertyChanged(nameof(moreThings));
}
}
public int stuff
{
get
{
return _stuff;
}
set
{
_stuff = value;
NotifyPropertyChanged(nameof(stuff));
}
}
public int moreStuff
{
get
{
return _moreStuff;
}
set
{
_moreStuff = value;
NotifyPropertyChanged(nameof(moreStuff));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
By setting the dataGrid's item source in the mainWindow constructor, it will automatically create the headers in the dataGrid based on the class variable names. Whenever you add an instance of Stuff'NThings (via button, other, whatever, and etc) to the observable collection, the trigger is thrown and it updates the UI. Hope some of this actually applies!
I think it may be quite simple but I have been googling a solution for a while without success, and I am not sure what is the correct approach. I saw that Prism could be a solution but I am looking for something simple.
I have a MainViewModel and a MainView which contains a Datagrid. The MainView contains also a ContentControl which display a ChildView (with DataContext as ChildViewModel).
I use DataTemplate to associate Views and ViewModels.
The DataGrid of the MainView is Binded to an ObservableCollection of the ChildViewModel.
I want this DataGrid to be updated every time this Collection is modified.
I have tried to use the INotifyPropertyChanged.
I have tried to use the OnCollectionChanged.
When I debug I can see that the Collection has changed and that event is fired but how to refresh the binding ? (dataGrid is not updated).
Here is the code:
DataTemplate
<DataTemplate DataType="{x:Type vm:ChildViewModel}">
<vi:ChildView />
</DataTemplate>
XAML MainView
<ContentControl Content="{Binding childViewModel}"/>
<DataGrid x:Name="DataGridChildren"
ItemsSource="{Binding childViewModel.Children,Mode=TwoWay}"
SelectedItem="{Binding childViewModel.SelectedItem}" EnableRowVirtualization="True" IsReadOnly="True" AutoGeneratingColumn="DataGrid_AutoGeneratingColumn">
MainViewModel
public ChildViewModel childViewModel { get; set; }
public MainViewModel()
{
childViewModel = new ChildViewModel();
}
ViewModelBase
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
var handler = PropertyChanged;
handler?.Invoke(this, args);
}
}
ChildViewModel
public class ChildViewModel: ViewModelBase
{
private ObservableCollection<Child> _Children;
public ObservableCollection<Child> Children
{
get { return _Children; }
set
{
_Children = value;
OnPropertyChanged("Children");
_Children.CollectionChanged +=handler;
}
}
private void handler(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//What to do ?
}
public ChildViewModel()
{
Children = new ObservableCollection<Child>(ChildService.GetAllChild());
}`
Edit #lezhkin11
Yes I think that is the problem. But I have no idea how to fix it
Code behind MainView
void MainView_Loaded(object sender, RoutedEventArgs e)
{
mainViewModel = new MainViewModel();
DataContext = mainViewModel;
}
Code behind ChildView
void ChildView_Loaded(object sender, RoutedEventArgs e)
{
childViewModel = new ChildViewModel();
DataContext = childViewModel;
}
A button allow to do a new search:
private void BtnRefresf_Click(object sender, RoutedEventArgs e) // Search
{
childViewModel.Search();
}
Then a method in the childViewModel will use the service to give a new value to the Observable collection (when I debug, I can reach the OnpropertyChanged of the collection)
public void Search()
{
ObservableCollection<Child> collec = new ObservableCollection<Child>(ChildService.GetAllChild());
Children = collec;
}
Your DataGrid is bound to ChildViewModel inside MainViewModel (instance 1). While ChildView has it's own ChildViewModel (instance 2).
You have to make sure that both controls reference to the same instance.
1) Remove completely public ChildViewModel childViewModel { get; set; } from MainViewModel
2) Give a name to the ChildView.
<vi:ChildView x:Name="MyChild" />
3) Bind DataGrid's properties to correct view-model
<DataGrid ItemsSource="{Binding DataContext.Children, ElementName=MyChild, Mode=TwoWay}" />
Also, I would recommend initializing DataContext directly in constructor.
Loaded event is too late - as result you always have lot's of XAML binding exceptions that affect performance (even can lead to unexpected behavior)
I have a panel and my idea is to have it populated by a stack panel containing two text boxes. When the user enters something in the left box, something should be generated in the right one, as follows.
<StackPanel Orientation="Horizontal">
<TextBox Name="Lefty" LostFocus="FillMyBuddy" />
<TextBox Name="Righty" LostFocus="FillMyBuddy" />
</StackPanel>
However, I'd like to add an option to add/remove rows and, since I wish not to limit myself to the number of such, I get a bit uncertain regarding the approach on two points.
Manipulating DOM (well, it's XAML/WPF but you see what I'm aiming at).
Event handling.
Is it a big no-no to programmatically affect the mark-up structure of the window? Or is it OK to add/remove panels during run-time?
What would the recommended way to be if I want the Lefty number 3 change stuff in Righty number 3? Anything more neat than checking the sender and pulling its siblings from the parent? I want to use a single event handler for any and all rows (knowing that the operations are always intra-row-wise).
You will want to follow MVVM, and have no code in your code-behind (programmatically affect the mark-up structure) files. The concept is easy when you grasp it, so learn it before you start writing your code.
In short, you are going to want to have a view model (something that implements INotifyPropertyChanged (INPC)) which holds your collection of items (which are going to be models, or view models in pure-MVVM). In "hybrid"-MVVM you could just have your models implement INPC.
Then, through the use of commands, you'd implement the logic to remove items from the list that its in. You can pass references, raise notification, using event bubbling, etc. (it's your preference) to have the item actually removed. In my case, I just passed a "manager" to the hybrid-model and held a reference to that. When the command is called (button is clicked), the model calls for the reference to remove itself from the list.
After you do that you define a DataTemplate to define what an "item" should look like one the View. You use a ItemsControl to show a collection of items, and bind to its ItemsSource so the collection of items are shown. Set your ItemsControl.ItemTemplate to the DataTemplate you created, and anything added to the collection bound to ItemsSource of the type defined in DataTemplate.DataType will render as you specify in the DataTemplate.
At the end of the day, you should learn about MVVM design, DataContext, INPC, Commands, Control types and their "main" properties, e.g. everything that inherits from ItemsControl has an ItemsSource property.
Here is a working example, where changing the original string, will reverse it and put it in the read-only right side text box:
MainWindow.xaml.cs (code-behind)
public partial class MainWindow : Window
{
StructureVm _struct = new StructureVm("Test");
public MainWindow()
{
InitializeComponent();
DataContext = _struct;
}
}
MainWindow.xaml (View)
<Window x:Class="DataTemplateWithCommands.MainWindow"
xmlns:local="clr-namespace:DataTemplateWithCommands"
Title="MainWindow" Height="350" Width="525" Background="Orange">
<Window.Resources>
<DataTemplate DataType="{x:Type local:Model}"
x:Key="VmItem">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Original, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding Encoded}"
IsReadOnly="True" />
<Button Content="X"
Command="{Binding RemoveMeCommand}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource VmItem}">
</ItemsControl>
</Grid>
</Window>
Interface (helpful for Dependency Injection)
public interface IStructureManager
{
bool RemoveItem(Model itemToRemove);
}
ViewModel
public class StructureVm : IStructureManager
{
private readonly ObservableCollection<Model> _items;
private readonly string _title;
public StructureVm(string title)
{
_title = title;
_items = new ObservableCollection<Model>
{
new Model(this, "12"),
new Model(this, "23"),
new Model(this, "34"),
new Model(this, "45"),
new Model(this, "56"),
new Model(this, "67"),
new Model(this, "78"),
new Model(this, "89"),
};
}}
public ObservableCollection<Model> Items
{
get
{
return _items;
}
}
public string Title
{
get
{
return _title;
}
}
public bool RemoveItem(Model itemToRemove)
{
return _items.Remove(itemToRemove);
}
}
Model (not pure-MVVM, pure MVVM models don't implement INPC, and don't have Command in them)
public class Model : INotifyPropertyChanged
{
private readonly RelayCommand _removeMe;
private string _original;
private string _encoded;
private readonly IStructureManager _manager;
public string Original
{
get
{
return _original;
}
set
{
_original = value;
Encoded = ReverseString(_original);
NotifyPropertyChanged();
}
}
public string Encoded
{
get
{
return _encoded;
}
set
{
_encoded = value;
NotifyPropertyChanged();
}
}
public ICommand RemoveMeCommand
{
get
{
return _removeMe;
}
}
public Model(IStructureManager manager, string original)
{
Original = original;
_manager = manager;
_removeMe = new RelayCommand(param => RemoveMe(), param => CanRemoveMe);
}
private void RemoveMe()
{
_manager.RemoveItem(this);
}
private bool CanRemoveMe
{
get
{
//Logic to enable/disable button
return true;
}
}
private string ReverseString(string s)
{
char[] arr = s.ToCharArray();
Array.Reverse(arr);
return new string(arr);
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName]string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
RelayCommand implementation
From here on out all you have to do is change the attributes of your controls to whatever you're happy with and call it good. The example might be ugly, but I'm leaving it as an exercise for you to figure out other properties/attributes of WPF controls.
In an attempted MVVM implimentation, I have a UserControl implementing INotifyPropertyChanged. OnFilePathsChanged seems to be called after FilePaths has the new value, so it just copies it over again. If I don't implement INotifyPropertyChanged OnPropertyChanged the VIEWMODEL never gets updated. I have read that using INotifyPropertyChanged together with a DependencyProperty is redundant but I can't figure out the best way to do this. What am I missing. Is there a cleaner way to implement a UserControl with a bound ObservableCollection?
UserControl code
private void LoadFileMenuItem_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new OpenFileDialog();
if (dialog.ShowDialog() == true)
{
FilePaths.Add(dialog.FileName);
OnPropertyChanged("FilePaths");
}
}
private void InitFilePathsProperty()
{
this.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(FilePaths_PropertyChanged);
}
void FilePaths_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
this.SetValue(FilePathsProperty, ImageFilePaths);
}
public static readonly DependencyProperty FilePathsProperty = DependencyProperty.Register("FilePaths", typeof(ObservableCollection<string>), typeof(FileDisplayControl),
new PropertyMetadata(OnImageFilePathsChanged));
private static void OnFilePathsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.NewValue != null)
{
var myFileDisplayControl = obj as FileDisplayControl;
if (myFileDisplayControl != null &&
myFileDisplayControl.FilePaths != null)
{
FilePaths = this.GetValue(args.Property) as ObservableCollection<string>;
}
}
}
public ObservableCollection<string> FilePaths
{
get { return this.GetValue(FilePathsProperty) as ObservableCollection<string>; }
set { this.SetValue(FilePathsProperty, value); }
}
In the VIEWMODEL
public ObservableCollection<string> ViewModelFilePaths
{
get { return _viewModelFilePaths; }
set { _viewModelFilePaths = value; }
}
It's a bit unclear as to what you're actually asking, but I think that this will help you whatever you're asking. If you have a view model that is data bound to a UserControl.DataContext, you can still data bind to any DependencyPropertys defined in that control using a RelativeSource Binding. Take this example from inside the UserControl:
Binding to the UserControl DependencyProperty:
<ListBox ItemsSource="{Binding FilePaths, RelativeSource={RelativeSource
AncestorType={x:Type YourPrefix:YourUserControl}}}" ... />
Binding to the data bound view model property:
<ListBox ItemsSource="{Binding ViewModelFilePaths}" ... />
You should implement the INotifyPropertyChanged interface in the view model, not the UserControl... at least, it is more customary to declare DependencyPropertys in a UserControl. However, if you have declared normal CLR properties that you want to data bind to in the UserControl, then you should implement the INotifyPropertyChanged interface there too.