FindVisualChild - Get properties of named UI Elements in ItemsControl - c#

I generate UI elements at runtime by using a ItemsControl. The UI generates successfully but if I am unable to get any properties of the generated UI items, such as "Content" for a label, or SelectedItem for a ComboBox. I tried to get these properties by using this tutorial , and these answers but I always get a NullReferenceException.
The ItemsControl in XAML looks like this:
<ItemsControl Name="ListOfVideos">
<ItemsControl.Background>
<SolidColorBrush Color="Black" Opacity="0"/>
</ItemsControl.Background>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition Width="400"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<Image HorizontalAlignment="Left" Height="100" Width="175" x:Name="VideoThumbnailImage" Stretch="Fill" Source="{Binding VideoThumbnailURL}" Grid.Column="0"></Image>
<Label x:Name="VideoTitleLabel" Content="{Binding VideoTitleText}" Foreground="White" Grid.Column="1" VerticalAlignment="Top" FontSize="16" FontWeight="Bold"></Label>
<Label x:Name="VideoFileSizeLabel" Content="{Binding VideoTotalSizeText}" Foreground="White" FontSize="14" Grid.Column="1" Margin="0,0,0,35" VerticalAlignment="Bottom"></Label>
<Label x:Name="VideoProgressLabel" Content="{Binding VideoStatusText}" Foreground="White" FontSize="14" Grid.Column="1" VerticalAlignment="Bottom"></Label>
<ComboBox x:Name="VideoComboBox" SelectionChanged="VideoComboBox_SelectionChanged" Grid.Column="2" Width="147.731" Height="20" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="0,0,0,50" ItemsSource="{Binding VideoQualitiesList}"></ComboBox>
<Label Content="Video Quality" Foreground="White" FontSize="14" VerticalAlignment="Top" Grid.Column="2" HorizontalAlignment="Center"></Label>
<Label Content="Audio Quality" Foreground="White" FontSize="14" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="0,0,0,27" Grid.Column="2"></Label>
<Slider x:Name="VideoAudioSlider" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Bottom" Width="147.731" Maximum="{Binding AudioCount}"></Slider>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This is how I generate the UI elements
public class VideoMetadataDisplay
{
public string VideoTitleText { get; set; }
public int AudioCount { get; set; }
public string VideoThumbnailURL { get; set; }
public string VideoStatusText { get; set; }
public string VideoTotalSizeText { get; set; }
public List<string> VideoQualitiesList { get; set; }
}
public partial class PlaylistPage : Page
{
private void GetPlaylistMetadata()
{
List<VideoMetadataDisplay> newList = new List<VideoMetadataDisplay>();
//populate the list
ListOfVideos.ItemsSource = newList;
}
}
And this is how I'm trying to get the properties of the UI elements
public class Utils
{
public childItem FindVisualChild<childItem>(DependencyObject obj)
where childItem : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is childItem)
{
return (childItem)child;
}
else
{
childItem childOfChild = FindVisualChild<childItem>(child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}
}
private void VideoComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
UIElement CurrentItem = (UIElement)ListOfVideos.ItemContainerGenerator.ContainerFromItem(ListOfVideos.Items.CurrentItem);
Utils utils = new Utils();
ContentPresenter CurrentContentPresenter = utils.FindVisualChild<ContentPresenter>(CurrentItem);
DataTemplate CurrentDataTemplate = CurrentContentPresenter.ContentTemplate;
Label VideoTitle = (Label)CurrentDataTemplate.FindName("VideoTitleLabel", CurrentContentPresenter);
string VideoTitleText = VideoTitle.Content.ToString();
MessageBox.Show(VideoTitleText);
}
Every time I try to run this, FindVisualChild always returns one of the labels (VideoTitleLabel) instead of returning the ContentPresenter for the currently active item. CurrentDataTemplate is then null and I am unable to get any of the UI elements from it.

It is impossible that FindVisualChild<ContentPresenter> returns a Label instance. FindVisualChild casts the result to ContentPresenter. Since Label is not a ContentPresenter, this would throw an InvalidCastException. But before this, child is childItem would return false in case child is of type Label and the generic parameter type childItem is of type ContentPresenter and therefore a potential null is returned.
Short Version
Accessing the the DataTemplate or looking up controls just to get their bound data is always too complicated. It's always easier to access the data source directly.
ItemsControl.SelectedItem will return the data model for the selected item. You are usually not interested in the containers.
private void VideoComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listView = sender as ListView;
var item = listView.SelectedItem as VideoMetadataDisplay;
MessageBox.Show(item.VideoTitleText);
}
Your Version (FindVisualChild improved)
FindVisualChild is weakly implemented. It will fail and throw an exception if traversal encounters a child node without children i.e. the parameter obj is null. You have to check the parameter obj for null before invoking VisualTreeHelper.GetChildrenCount(obj), to avoid a null reference.
Also you don't need to search the element by accessing the template. You can look it up directly in the visual tree.
I have modified your FindVisualChild method to search elements by name. I also have turned it into an extension method for convenience:
Extension Method
public static class Utils
{
public static bool TryFindVisualChildByName<TChild>(
this DependencyObject parent,
string childElementName,
out TChild childElement,
bool isCaseSensitive = false)
where TChild : FrameworkElement
{
childElement = null;
// Popup.Child content is not part of the visual tree.
// To prevent traversal from breaking when parent is a Popup,
// we need to explicitly extract the content.
if (parent is Popup popup)
{
parent = popup.Child;
}
if (parent == null)
{
return false;
}
var stringComparison = isCaseSensitive
? StringComparison.Ordinal
: StringComparison.OrdinalIgnoreCase;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (child is TChild resultElement
&& resultElement.Name.Equals(childElementName, stringComparison))
{
childElement = resultElement;
return true;
}
if (child.TryFindVisualChildByName(childElementName, out childElement))
{
return true;
}
}
return false;
}
}
Example
private void VideoComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listView = sender as ListView;
object item = listView.SelectedItem;
var itemContainer = listView.ItemContainerGenerator.ContainerFromItem(item) as ListViewItem;
if (itemContainer.TryFindVisualChildByName("VideoTitleLabel", out Label label))
{
var videoTitleText = label.Content as string;
MessageBox.Show(videoTitleText);
}
}

Related

How to bind and display ListBoxItem index in ListBox?

I have a .NET 5 project with following Nuget Packages:
HandyControls for UI
Gong-Wpf-DragDrop for Drag and Drop elements in a List
I have a XAML with a ListBoxand a ViewModel with aObservableCollection` of Model.
The ObservableCollection is binded as ItemSource of ListBox
What I want to achieve:
When i Drag and Drop an item in a different position (or Add/Delete), I want the indexes to be refreshed.
Example:
Before Drag/Drop
After Drag/Drop
Actually, i binded the drophandler of gong-wpf-dragdrop
and at the end of the drop, i manually refresh every single Index in my list.
there is a way to do it easily? because actually i have to refresh indexes manually.
Summarizing:
When i reorder/delete/add items i want Model.Index of every item updated with the correct index position in ListBox.
My Mandate is:
Show index (one based)
Give the possibility to reorder the elements
I tried looking for similar questions but didn't find much that could help me.
Thanks in advance :)
Model:
public class Model : BindablePropertyBase
{
private int index;
private string name;
public int Index
{
get { return index; }
set
{
index = value;
RaisePropertyChanged();
}
}
public string Name
{
get { return name; }
set
{
name = value;
RaisePropertyChanged();
}
}
}
And below a xaml with a simple binded list
MainWindow.xaml
<hc:Window x:Class="ListBoxIndexTest.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:hc="https://handyorg.github.io/handycontrol"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dd="clr-namespace:GongSolutions.Wpf.DragDrop;assembly=GongSolutions.Wpf.DragDrop"
xmlns:local="clr-namespace:ListBoxIndexTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="600">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<ListBox ItemsSource="{Binding TestList}" dd:DragDrop.DropHandler="{Binding}"
dd:DragDrop.IsDropTarget="True" dd:DragDrop.IsDragSource="True">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="10"
Background="Aqua">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Index}" Margin="10,0"/>
<TextBlock Text="{Binding Name}"
Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</hc:Window>
MainWindowViewModel.cs
public class MainWindowViewModel : BindablePropertyBase, IDropTarget
{
private ObservableCollection<Model> test_list;
public ObservableCollection<Model> TestList
{
get
{
return test_list;
}
set
{
test_list = value;
RaisePropertyChanged();
}
}
// Constructor
public MainWindowViewModel()
{
TestList = new ObservableCollection<Model>()
{
new Model()
{
Index = 1,
Name = "FirstModel"
},
new Model()
{
Index = 2,
Name = "SecondModel"
},
new Model()
{
Index = 3,
Name = "ThirdModel"
}
};
}
public void DragOver(IDropInfo dropInfo)
{
Model sourceItem = dropInfo.Data as Model;
Model targetItem = dropInfo.TargetItem as Model;
if (sourceItem != null && targetItem != null)
{
dropInfo.DropTargetAdorner = DropTargetAdorners.Insert;
dropInfo.Effects = DragDropEffects.Move;
}
}
public void Drop(IDropInfo dropInfo)
{
Model sourceItem = dropInfo.Data as Model;
Model targetItem = dropInfo.TargetItem as Model;
if(sourceItem != null && targetItem != null)
{
int s_index = sourceItem.Index - 1;
int t_index = targetItem.Index - 1;
TestList.RemoveAt(s_index);
TestList.Insert(t_index, sourceItem);
RefreshAllIndexes();
}
}
private void RefreshAllIndexes()
{
for (int i = 0; i < TestList.Count; i++)
{
TestList[i].Index = i + 1;
}
}
}
I don't believe there is an out-of-the box way to bind to a container index in WPF. Your solution is actually easy to understand.
If you find yourself binding often to index, you could create your own attached property/value converter that internally climbs up the visual tree using these helpers until it finds the parent ItemsControland makes use of the IndexFromContainer method.
Here is some code to get you started with this method:
First a small helper function to climb up the visual tree looking for an item of generic type:
public static DependencyObject FindParentOfType<T>(DependencyObject child) where T : DependencyObject {
//We get the immediate parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) {
return null;
}
//check if the parent matches the type we're looking for
if (parentObject is T parent) {
return parent;
} else {
return FindParentOfType<T>(parentObject);
}
}
Then a value converter that takes a control as input value and returns its index in the first encountered ItemsControl:
public class ContainerToIndexConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
//Cast the passed value as an ItemsControl container
DependencyObject container = value as ContentPresenter;
if (container == null) {
container = value as ContentControl;
}
//Finds the parent ItemsControl by looking up the visual tree
var itemControls = (ItemsControl)FindParentOfType<ItemsControl>(container);
//Gets the index of the container from the parent ItemsControl
return itemControls.ItemContainerGenerator.IndexFromContainer(container);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
And this is how you would use it in XAML:
<ListBox.ItemTemplate>
<DataTemplate>
<!-- This will display the index of the list item. -->
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentPresenter}, Converter={StaticResource ContainerToIndexConverter}}" />
</DataTemplate>
</ListBox.ItemTemplate>

WPF button content is empty when shown

I am new to C#/WPF. There is a view with one button defined, when the view is initialized, buttons will display a set of reason codes got from DataContext (viewmodel), once any button is clicked, the code on it will be saved and passed forward for next processing.
Q: The text on buttons are totally empty, but the clicked code can be captured, so where the problem is about binding? Thanks.
XAML:
<Button x:Name="btnReason" Command="{Binding DataContext.SelectCommand, RelativeSource={RelativeSource AncestorType=v:View, Mode=FindAncestor}}" CommandParameter="{Binding}" Width="190" Height="190" >
<Border Background="Transparent">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock x:Name="Reason" Grid.Row="0" Text="{Binding ?????}" TextWrapping="Wrap" />
</Grid>
</Border>
</Button>
The code on C#:
public class ReasonsViewModel : ViewModel
{
private IEnumerable<string> m_Names;
public IEnumerable<string> Names
{
get { return m_Names; }
set
{
if (m_Names != value)
{
m_Names = value;
OnPropertyChanged(() => Names);
}
}
}
private string m_SelectedName;
public string SelectedName
{
get { return m_SelectedName; }
set
{
if (m_SelectedName != value)
{
m_SelectedName = value;
OnPropertyChanged(() => SelectedName);
}
}
}
public DelegateCommand SelectCommand { get; private set; }
public ReasonsViewModel()
{
SelectCommand = new DelegateCommand(p => SelectCommandExecute(p));
}
private bool m_Processing;
private void SelectCommandExecute(object item)
{
if (m_Processing) return;
try
{
m_Processing = true;
var name = item as string;
if (name == null) return;
SelectedName = name;
}
finally
{
m_Processing = false;
}
}
}
If I understood your question correctly than your property text in your TextBlock should be bound to SelectedName.
The problem is that your CommandParameter is bound to DataContext. That's what an empty {Binding} statement bounds to. This means your command handler always returns after the null check.
I also suggest that you change your Names proeprty from IEnumerable<string> to ObservableCollection<string>.
ObservableCollection raises events on any additions or removalof items inside and WPF components can bind to these events.

Listbox inside pivot

I've got this code. I need to have access to the ScheduleList from my c# code. But it's inaccessible. I can get access to SchedulePivot only.
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="0,0,0,50">
<Pivot x:Name="SchedulePivot" Margin="10,10,10,0" Title="Pivot" VerticalAlignment="Top">
<Pivot.ItemTemplate>
<DataTemplate>
<ListBox x:Name="ScheduleList" Margin="0,0,0,17" Width="Auto">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Height="52" Width="auto">
Searching on StackOverflow I have found this code:
private DependencyObject FindChildControl<T>(DependencyObject control, string ctrlName)
{
int childNumber = VisualTreeHelper.GetChildrenCount(control);
for (int i = 0; i < childNumber; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(control, i);
FrameworkElement fe = child as FrameworkElement;
// Not a framework element or is null
if (fe == null) return null;
if (child is T && fe.Name == ctrlName)
{
// Found the control so return
return child;
}
else
{
// Not found it - search children
DependencyObject nextLevel = FindChildControl<T>(child, ctrlName);
if (nextLevel != null)
return nextLevel;
}
}
return null;
}
I use this line to get the child:
ListBox listCont = FindChildControl<ListBox>(this, "ScheduleList") as ListBox;
Also I tried doing like this:
ListBox listCont = FindChildControl<ListBox>(SchedulePivot, "ScheduleList") as ListBox;
than I do this:
listCont.Items.Add(items);
And get the exeption as listCont=null. What's wrong I'm doing?
I have tested your code, both of the following code work well in my side and I can get the correct result:
ListBox listCont = FindChildControl<ListBox>(this, "ScheduleList") as ListBox;
ListBox listCont = FindChildControl<ListBox>(SchedulePivot, "ScheduleList") as ListBox;
If we want to access the control by using the VisualTreeHelper, we should make sure that we have not called the above code inside the constructor of the MainPage, or we will get the null result as below. Because the control does not been initialized completely:
In order to get the correct result, we need to call the above code inside the MainPage.Loaded event or Button click event to make sure that control has been initialized completely, after that it should work fine.
The following is my sample, please try to refer to:
In the MainPage.xaml:
<Pivot x:Name="SchedulePivot" ItemsSource="{Binding PivotTestlist}" Margin="10,10,10,0" Title="Pivot" VerticalAlignment="Top">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding header}"></TextBlock>
</DataTemplate>
</Pivot.HeaderTemplate>
<Pivot.ItemTemplate>
<DataTemplate>
<ListBox x:Name="ScheduleList" Margin="0,0,0,17" Width="Auto" ItemsSource="{Binding ListBoxTestlist}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Height="52" Width="auto">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding id}"></TextBlock>
<TextBlock Text="{Binding name}"></TextBlock>
</StackPanel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
<Button Click="Button_Click" Content="Button"></Button>
In the MainPage.xaml.cs:
public class ListBoxTest
{
public string name { get; set; }
public string id { get; set; }
}
public class PivotTest
{
public List<ListBoxTest> ListBoxTestlist { get; set; }
public string header { get; set; }
}
public sealed partial class MainPage : Page
{
public List<PivotTest> PivotTestlist { get; set; }
public MainPage()
{
this.InitializeComponent();
PivotTestlist = new List<PivotTest>();
PivotTest PivotTest1 = new PivotTest();
PivotTest1.ListBoxTestlist = new List<ListBoxTest>();
PivotTest1.ListBoxTestlist.Add(new ListBoxTest() { name = "name1", id = "id1" });
PivotTest1.ListBoxTestlist.Add(new ListBoxTest() { name = "name2", id = "id2" });
PivotTest1.header = "header1";
PivotTestlist.Add(PivotTest1);
PivotTest PivotTest2 = new PivotTest();
PivotTest2.ListBoxTestlist = new List<ListBoxTest>();
PivotTest2.ListBoxTestlist.Add(new ListBoxTest() { name = "name11", id = "id11" });
PivotTest2.ListBoxTestlist.Add(new ListBoxTest() { name = "name22", id = "id22" });
PivotTest2.header = "header2";
PivotTestlist.Add(PivotTest2);
this.DataContext = this;
}
private DependencyObject FindChildControl<T>(DependencyObject control, string ctrlName)
{
int childNumber = VisualTreeHelper.GetChildrenCount(control);
for (int i = 0; i < childNumber; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(control, i);
FrameworkElement fe = child as FrameworkElement;
// Not a framework element or is null
if (fe == null) return null;
if (child is T && fe.Name == ctrlName)
{
// Found the control so return
return child;
}
else
{
// Not found it - search children
DependencyObject nextLevel = FindChildControl<T>(child, ctrlName);
if (nextLevel != null)
return nextLevel;
}
}
return null;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ListBox listCont = FindChildControl<ListBox>(SchedulePivot, "ScheduleList") as ListBox;
int count = listCont.Items.Count;
}
}
The result:
Declare the Pivot in a storyboard and use the x:Key instead of x:Name.
e.g.
<StoryBoard>
<Pivot x:key="nameIt"/>
</StoryBoard>
private void AccesPivot ()
{ //now you can acces your pivot
}

Bound observable collection update without 'RemoveOf' & 'Insert'

Currently I am trying to implement an observable collection which is bound to a data template (WPF-MVVM). During initialization it loads the default value to observable collection. Idea is:
User provides some value on the textbox,
presses ENTER key
increments a counter and updates the count value on textblock which is located near the text box.
The purpose is to track how times the text value has been changed by the user.
Right now it is working with 'IndexOf', 'RemoveAt' and 'Insert'. Is there a way to do without 'RemoveAt' and 'Insert'.
I feel something wrong on my code? Can anybody help it.
InputDataTemplate.xaml:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" HorizontalAlignment="Center" VerticalAlignment="Center" Content="{Binding Name}" />
<Label Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center" Content="{Binding Count}" />
<TextBox x:Name="IpDataTb" Grid.Column="1" Width="60" HorizontalAlignment="Center" VerticalAlignment="Center" DataContext="{Binding}" Text="{Binding Path=Data, Mode=TwoWay}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyDown">
<ei:CallMethodAction TargetObject="{Binding }" MethodName="IpDataTrig" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
</Grid>
TestView.xaml:
<UserControl.Resources>
<DataTemplate x:Key="InputDataTemplate" >
<local:InputDataTemplate DataContext="{Binding}" />
</DataTemplate>
</UserControl.Resources>
<Grid>
<Border BorderBrush="#FF0254B4" BorderThickness="1" >
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" >
<ItemsControl ItemsSource="{Binding InputDatas}"
ItemTemplate="{DynamicResource InputDataTemplate}" />
</ScrollViewer>
</Border>
</Grid>
DataService.cs:
using MyObsrCollTest.ViewModels;
namespace MyObsrCollTest.Services
{
public class InputDataService : BindableBase
{
public string Name { get; set; }
public string Count { get; set; }
public string Data { get; set; }
public void IpDataTrig(object sender,KeyEventArgs e)
{
var IpDataTb = new TextBox();
IpDataTb = (TextBox)sender;
if ((e.Key == Key.Enter) &&(!string.IsNullOrWhiteSpace(IpDataTb.Text)))
{
this.Data = IpDataTb.Text;
ObsrCollTestVm.TestMe(this.Name, this.Data);
}
}
}
}
ObsrCollTestVm.cs:
private ObservableCollection<InputDataService> _InputDatas;
static int _count = 0;
public ObsrCollTestVm(void)
{
for (int i = 0; i < 5; i++)
{
var l_InputDatas = new InputDataService();
l_InputDatas.Name = i.ToString();
l_InputDatas.Count = "0";
l_InputDatas.Data = "?";
_InputDatas.Add(l_InputDatas);
}
}
Basic initialization routine:
public ObservableCollection<InputDataService> InputDatas
{
get
{
if (_InputDatas == null)
{
_InputDatas = new ObservableCollection<InputDataService>();
}
return _InputDatas;
}
}
New Observable collection:
public static void TestMe(string name, string data)
{
var found = _InputDatas.FirstOrDefault(element = > element.Name == name);
if (found != null)
{
int i = _InputDatas.IndexOf(found);
found.Count = _count++;
_InputDatas.RemoveAt(i);
_InputDatas.Insert(i, found);
}
}
Increment the count value:
If I understand the question correctly, it can be summarized as:
"I would like to be able to change the Count property of my InputDataService class objects and have that change reflected in that item's Label, without having to modify the ObservableCollection<InputDataService> itself."
Is that correct?
If so, then the solution is for your InputDataService class to correctly provide notifications of property changes. Normally, this would mean either inheriting DependencyObject and implementing your properties as dependency properties, or just implementing the INotifyPropertyChanged interface.
But in your example, you seem to be inheriting a class named BindableBase already. If that class is in fact the Microsoft.Practices.Prism.Mvvm.BindableBase class, then it already implements INotifyPropertyChanged and all you need to do is take advantage of that.
For example:
public class InputDataService : BindableBase
{
private int _count;
public string Name { get; set; }
public int Count
{
get { return _count; }
set { SetProperty(ref _count, value); }
}
public string Data { get; set; }
public void IpDataTrig(object sender,KeyEventArgs e)
{
var IpDataTb = new TextBox();
IpDataTb = (TextBox)sender;
if ((e.Key == Key.Enter) &&(!string.IsNullOrWhiteSpace(IpDataTb.Text)))
{
this.Data = IpDataTb.Text;
ObsrCollTestVm.TestMe(this.Name, this.Data);
}
}
}
Notes:
In the above, I only fixed the issue for the Count property. You can apply similar changes to the other properties to get them to update correctly.
In your TestMe() method, you seem to be using the Count property as an int, but it was declared in your code example as a string. Lacking a better way to reconcile that discrepancy in your code example, I've just changed the property declaration in the example above to use int instead of string.
This example assumes you are using .NET 4.5, in which the [CallerMemberName] attribute is supported. If you're using an earlier version of .NET, then you will need to add the property name to the SetProperty() call. E.g.: SetProperty(ref _count, value, "Count");
With these changes, you should be able to write TestMe() like this:
public static void TestMe(string name, string data)
{
var found = _InputDatas.FirstOrDefault(element = > element.Name == name);
if (found != null)
{
found.Count = _count++;
}
}

WPF MVVM moving a UserControl from one ObservableCollection to another by event

I have a checklist view that has 2 ScrollViewers. One checklist is for incomplete items, the other is for complete items. They are populated by 2 separate observable collections and bound to by ItemsControls.
The UserControl has a button, when clicked will move that 'check' to the other collection.
Currently the way I have this setup is in the ViewModel that's the DataContext for the UserControl there is a public event that is subscribed to by the main window's VM by using:
((CheckItemVM) ((CheckListItem) cli).DataContext).CompleteChanged += OnCompleteChanged;
where cli is the checklist item.
then the OnCompleteChanged finds the appropriate View object by using:
foreach (object aCheck in Checks)
{
if (aCheck.GetType() != typeof (CheckListItem)) continue;
if (((CheckListItem) aCheck).DataContext == (CheckItemVM) sender)
{
cliToMove = (CheckListItem) aCheck;
break;
}
}
It's pretty obvious this breaks MVVM and I'm looking for a way around it (CheckListItem is the View, and CheckItemVM is it's DataContext ViewModel). Reasoning for the boxed type is I've got another UserControl that will have instances inside both, which are basically section labels, and I need to be able to sort my observable collections where there is an association between the checklistitem to a specific section by name.
This can be done in MVVM using commands, and bindings....
The idea that I propouse here is to create a command in the Windows view model, that manage the check command, and this command to receive the item view model in the params, then manage the the things in the command. I'm going to show you a simple example, using MvvmLight library:
The model:
public class ItemViewModel : ViewModelBase
{
#region Name
public const string NamePropertyName = "Name";
private string _name = null;
public string Name
{
get
{
return _name;
}
set
{
if (_name == value)
{
return;
}
RaisePropertyChanging(NamePropertyName);
_name = value;
RaisePropertyChanged(NamePropertyName);
}
}
#endregion
#region IsChecked
public const string IsCheckedPropertyName = "IsChecked";
private bool _myIsChecked = false;
public bool IsChecked
{
get
{
return _myIsChecked;
}
set
{
if (_myIsChecked == value)
{
return;
}
RaisePropertyChanging(IsCheckedPropertyName);
_myIsChecked = value;
RaisePropertyChanged(IsCheckedPropertyName);
}
}
#endregion
}
A simple model with two property, one for the name (an identifier) and another for the check status.
Now in the Main View Model, (or Windows view model like you want)....
First the Collections, one for the checked items, and another for the unchecked items:
#region UncheckedItems
private ObservableCollection<ItemViewModel> _UncheckedItems;
public ObservableCollection<ItemViewModel> UncheckedItems
{
get { return _UncheckedItems ?? (_UncheckedItems = GetAllUncheckedItems()); }
}
private ObservableCollection<ItemViewModel> GetAllUncheckedItems()
{
var toRet = new ObservableCollection<ItemViewModel>();
foreach (var i in Enumerable.Range(1,10))
{
toRet.Add(new ItemViewModel {Name = string.Format("Name-{0}", i), IsChecked = false});
}
return toRet;
}
#endregion
#region CheckedItems
private ObservableCollection<ItemViewModel> _CheckedItems;
public ObservableCollection<ItemViewModel> CheckedItems
{
get { return _CheckedItems ?? (_CheckedItems = GetAllCheckedItems()); }
}
private ObservableCollection<ItemViewModel> GetAllCheckedItems()
{
var toRet = new ObservableCollection<ItemViewModel>();
foreach (var i in Enumerable.Range(11, 20))
{
toRet.Add(new ItemViewModel { Name = string.Format("Name-{0}", i), IsChecked = true });
}
return toRet;
}
#endregion
And the command:
#region CheckItem
private RelayCommand<ItemViewModel> _CheckItemCommand;
public RelayCommand<ItemViewModel> CheckItemCommand
{
get { return _CheckItemCommand ?? (_CheckItemCommand = new RelayCommand<ItemViewModel>(ExecuteCheckItemCommand, CanExecuteCheckItemCommand)); }
}
private void ExecuteCheckItemCommand(ItemViewModel item)
{
//ComandCode
item.IsChecked = true;
UncheckedItems.Remove(item);
CheckedItems.Add(item);
}
private bool CanExecuteCheckItemCommand(ItemViewModel item)
{
return true;
}
#endregion
The magic here could be in the Data binding, in this case I used command parameter and the FindAncestor binding, check the Data Template:
<DataTemplate x:Key="UncheckedItemDataTemplate">
<Grid>
<StackPanel Orientation="Horizontal">
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Name}" VerticalAlignment="Top"/>
<CheckBox HorizontalAlignment="Left" VerticalAlignment="Top" IsChecked="{Binding IsChecked}" IsEnabled="False"/>
<Button Content="Check" Width="75" Command="{Binding DataContext.CheckItemCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" CommandParameter="{Binding Mode=OneWay}"/>
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="CheckedItemDataTemplate">
<Grid>
<StackPanel Orientation="Horizontal">
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Name}" VerticalAlignment="Top"/>
<CheckBox HorizontalAlignment="Left" VerticalAlignment="Top" IsChecked="{Binding IsChecked}" IsEnabled="False"/>
</StackPanel>
</Grid>
</DataTemplate>
One data template for checked items, and another for unchecked items. Now the usage, this is simpler:
<ListBox Grid.Row="2" Margin="5" ItemsSource="{Binding UncheckedItems}" ItemTemplate="{DynamicResource UncheckedItemDataTemplate}"/>
<ListBox Grid.Row="2" Margin="5" Grid.Column="1" ItemsSource="{Binding CheckedItems}" ItemTemplate="{DynamicResource CheckedItemDataTemplate}"/>
This is a cleaner solution, hope is helps.

Categories