Window freezes when adding items to ObservableCollection - wpf

I have a DataGrid which is bound to an ObservableCollection ProductsFound
which is exposed as a property in my ViewModel.
By typing text in a TextBox, products contained in the model that have the Code property that contains the text inserted in the TextBox are added to ProductsFound.
I found out that if the DataGrid is contained in any control such as a StackPanel or a TabItem, the Window (the program) stops responding when I try to type text into the TextBox; while if the DataGrid isn't contained in any control, everything runs normally.
Here's the code for the window:
public class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// This method just fill the dataset I pass to the model's contructor in the next line.
Init();
ProductsModel model = new ProductsModel(dataSet);
searchViewModel = new ProductsSearchViewModel(model);
DataContext = searchViewModel;
}
private ProductsSearchViewModel searchViewModel;
// This handler supports the binding between the TextBox and the MatchText property of the View Model.
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var binding = ((TextBox)sender).GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();
}
}
And here's my ViewModel:
public class ProductsSearchViewModel : Notifier, IProductsSearchViewModel
{
public ProductsSearchViewModel(IProductsModel inModel)
{
model = inModel;
productsFound = new ObservableCollection<ProductViewModel>();
}
private string matchText;
private IProductsModel model;
private ObservableCollection<ProductViewModel> productsFound;
// This is a helper method that search for the products in the model and adds them to ProductsFound.
private void Search(string text)
{
Results.Clear();
foreach (Product product in model.Products)
{
if (product.Code.ToLower().Contains(text.ToLower()))
Results.Add(new ProductViewModel(product));
}
}
public string MatchText
{
get { return matchText; }
// This setter is meant to be executed every time the Text property of the TextBox is changed.
set
{
if ((value != matchText) && (value != ""))
{
matchText = value;
// This raises INotifyPropertyChanged.PropertyChaged.
NotifyPropertyChanged("MatchText");
Search(value);
}
}
}
public ObservableCollection<ProductViewModel> ProductsFound
{
get
{
return productsFound;
}
set
{
productsFound = value;
NotifyPropertyChanged("Results");
}
}
}
Here's the XAML:
<Window x:Class="MyNameSpace.UI.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>
<TextBox Text="{Binding MatchText, Mode=TwoWay}" TextChanged="TextBox_TextChanged" />
<DataGrid x:Name="grid1" ItemsSource="{Binding Results}" >
</StackPanel>
</Grid>
With that StackPanel the program stops responding when I try to type text in the Textbox and no item is added to the DataGrid; but if i remove it everything runs ok.
What could the problem be? Am I missing something in how the WPF binding system works?
Is my view model coded wrong?
Thanks in advance.

Putting that StackPanel there prevents the DataGrid from acquiring a specific Height, thus it just expands down to infinity, and that breaks UI Virtualization.
Remove the StackPanel from there and use a non-infinite container, such as Grid or DockPanel.

Related

How do I find what text had been added with TextChanged

I'm looking to synchronize between a text in the textbox and string in a variable. I found how to get the index in which the string was changed (in the textbox), the length added and length removed, but how can I actually find the string added?
So far I've used TextChangedEventArgs.Changes, and got the properties of the items in it (ICollection).
I'm trying to create a password box in which I could show the actual password by a function. hence I do not want the textbox to synchronize directly (for example, in the textbox would appear "*****" and in the string "hello").
If you want only text added you can do this
string AddedText;
private void textbox_TextChanged(object sender, TextChangedEventArgs e)
{
var changes = e.Changes.Last();
if (changes.AddedLength > 0)
{
AddedText = textbox.Text.Substring(changes.Offset,changes.AddedLength);
}
}
Edit
If you want all added and remove text you can do this
string oldText;
private void textbox_GotFocus(object sender, RoutedEventArgs e)
{
oldText = textbox.Text;
}
string AddedText;
string RemovedText;
private void textbox_TextChanged(object sender, TextChangedEventArgs e)
{
var changes = e.Changes.Last();
if (changes.AddedLength > 0)
{
AddedText = textbox.Text.Substring(changes.Offset, changes.AddedLength);
if (changes.RemovedLength == 0)
{
oldText = textbox.Text;
RemovedText = "";
}
}
if (changes.RemovedLength > 0)
{
RemovedText = oldText.Substring(changes.Offset, changes.RemovedLength);
oldText = textbox.Text;
if (changes.AddedLength == 0)
{
AddedText = "";
}
}
}
DataBinding is the most common way in WPF to show and collect data in a UI
Try this:
<Window x:Class="WpfApp3.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:WpfApp3"
mc:Ignorable="d"
Title="MainWindow"
Height="350"
Width="525">
<Grid>
<TextBox Text="{Binding Path=SomeText, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left"
Margin="101,83,0,0"
VerticalAlignment="Top"
Width="75" />
<TextBlock Text="{Binding SomeText}"
HorizontalAlignment="Left"
Margin="101,140,0,0"
VerticalAlignment="Top"
Width="75" />
</Grid>
</Window>
Code for the window:
public partial class MainWindow : Window
{
private readonly AViewModel viewModel = new AViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = viewModel;
}
}
And the code for the ViewModel that holds the data you want to show and collect:
public class AViewModel : INotifyPropertyChanged
{
private string someText;
public string SomeText
{
get
{
return someText;
}
set
{
if (Equals(this.someText, value))
{
return;
}
this.someText = value;
this.OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(
[CallerMemberName]string propertyName = null)
{
this.PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(propertyName));
}
}
Although this looks complicated for a simple scenario it has a lot of advantages:
You can write automated (unit)test for the ViewModel without creating a UI
Adding extra fields and logic is trivial
If the UI needs to change, the ViewModel will not always need to change
The core of the mechanism is the {Binding ...} bit in the Xaml that tell WPF to synchronize the data between the Text property of the TextBox and the SomeText property of the object that is assigned to the DataContext.
The other significant bits are:
- in the constructor of the window the setting of the DataContext and
- in the ViewModel the raising of the PropertyChanged event when the SomeText property changes so the binding will be notified.
Note that this is just a basic example of DataBinding, there are many improvements that could be made in this code.

Binding of Label not working

my label is showing no content . what i am trying to do is i have a usercontrol TemplateForPlan and i am getting the selected item from that usecontrol and after that i am coming to next usercontrol and that selected template name must be there in label content.
sorry for poor description . i am a newbie and just started to work on WPF.
<UserControl x:Class="ChaosMonkeyUI.TemplateForPlan"
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"
mc:Ignorable="d"
d:DesignHeight="344" d:DesignWidth="424" Name="TemplateForPlanUC">
and this this is the label on another UC to show selected template
<Label Content="{Binding ElementName=TemplateForPlanUC, Path=selectedTemplate.TemplateName }" Grid.Row="1" Grid.Column="1" Height="28" HorizontalAlignment="Stretch"
Name="labelTemplateName" VerticalAlignment="Stretch" Margin="10,5,0,5" />
this is .cs file of TemplateForPlan and
public partial class TemplateForPlan : UserControl
{
IList<TemplateType> template;
public int noOfElementSelected;
TemplateHelper xmlParser ;
NewChaosSteps parentNewChaosStepPageForNextButton;
public TemplateType selectedTemplate = null;
public TemplateForPlan( NewChaosSteps parentNewChaosStepPageForNextButton)
{
InitializeComponent();
this.parentNewChaosStepPageForNextButton = parentNewChaosStepPageForNextButton;
parentNewChaosStepPageForNextButton.EnableOrDisableNextButton("disable");
xmlParser = new TemplateHelper();
template = xmlParser.GetTemplates();
listTemplate.ItemsSource = template;
}
private void listTemplate_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
selectedTemplate = template[listTemplate.SelectedIndex];
parentNewChaosStepPageForNextButton.EnableOrDisableNextButton("enable");
}
and TemplateType is defined in other project and its defination is:
public partial class TemplateType
{
private TemplateRuleType[] templateRuleField;
private string templateNameField;
private string templateDescriptionField;
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute("TemplateRule")]
public TemplateRuleType[] TemplateRule {
get {
return this.templateRuleField;
}
set {
this.templateRuleField = value;
}
}
/// <remarks/>
[System.Xml.Serialization.XmlAttributeAttribute()]
public string TemplateName {
get {
return this.templateNameField;
}
set {
this.templateNameField = value;
}
}
/// <remarks/>
[System.Xml.Serialization.XmlAttributeAttribute()]
public string TemplateDescription {
get {
return this.templateDescriptionField;
}
set {
this.templateDescriptionField = value;
}
}
}
please also give some good link so that i can properly understand binding . i am very much confused in it.
You cannot bind to a field.
listTemplate is an items control, so it will have a SelectedItem property which you can bind to a property in your code behind.
public TemplateType SelectedTemplate { get; set; }
Then change your Label binding:
<Label Content="{Binding ElementName=TemplateForPlanUC, Path=SelectedTemplate.TemplateName }" />
(Notice the change in capitalisation of the name in the Path. If you post the XAML for your ItemsControl in TemplateForPlanUC then I will include an example that suits your case in my answer).
You also need to ensure you implement INotifyPropertyChanged on your control, and ensure that your SelectedTemplate property notifies in its setter. I won't detail that here because it has been covered a billion times before here on StackOverflow.

Behaviour of ItemsControl

I have a screen with several UserControls, but only one of them remains active. The other UserControls aren't shown, but the user can switch the active flag of any of those who are not active. One of the UserControl contains an ItemsControl.
I need to know all the controls in the view, including those generated by an ItemsControl, after loading the first UserControl that is active in the screen, when view is finally initialized.
For ItemsControl, wpf didn't instance any item until it was painted on the screen that contains the UserControl (so I've tried, until the Load event is launched), so that I can't found the controls contained by the view because it didn't exist.
Is there any way to change this behavior?
I try to change the value of property VirtualizingStackPanel.IsVirtualizing to false, to avoid the previous behaviour, with no success. To illustrate this, I write this view example:
<Window x:Class="ContenidoEnTabs.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">
<StackPanel x:Name="spContainer" Orientation="Vertical" VirtualizingStackPanel.IsVirtualizing="False">
<Button Content="Push" Click="Button_Click" />
</StackPanel>
</Window>
This view creates a second control not visible until the user press the button:
public partial class MainWindow : Window
{
private NotPaintedOnInitUserControl controlExtra;
public MainWindow()
{
InitializeComponent();
controlExtra = new NotPaintedOnInitUserControl();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
spContainer.Children.Add(controlExtra);
}
}
The control not visible initially is as follow:
<UserControl x:Class="ContenidoEnTabs.NotPaintedOnInitUserControl"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<ItemsControl ItemsSource="{Binding MyCollection}" x:Name="itemsControlTarget"
VirtualizingStackPanel.IsVirtualizing="False">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBox x:Name="aTextBox" Width="80" Initialized="ATextBox_Initialized" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>
and in CodeBehind I detect when the Items were created
public partial class NotPaintedOnInitUserControl : UserControl
{
public NotPaintedOnInitUserControl()
{
InitializeComponent();
DataContext = new SimpleListDataContext();
}
private void ATextBox_Initialized(object sender, EventArgs e)
{
}
}
And the DataContext used:
public class SimpleListDataContext
{
private List<string> _myCollection;
public List<string> MyCollection
{
get { return _myCollection ?? (_myCollection = new List<string> { "one", "two" }); }
set { _myCollection = value; }
}
}
Any ideas?
Thanks in advance.
If you want WPF to generate the tree for a control that isn't part of the view, you can "hydrate" and layout the control by forcing the layout to run. Something like this should work:
public partial class MainWindow : Window
{
private NotPaintedOnInitUserControl controlExtra;
public MainWindow()
{
InitializeComponent();
controlExtra = new NotPaintedOnInitUserControl();
// Force the control to render, even though it's not on the screen yet.
var size = new Size(this.Width, this.Height);
var rect = new Rect(new Point(0,0), size);
controlExtra.Measure(size);
controlExtra.Arrange(rect);
controlExtra.InvalidateVisual();
controlExtra.UpdateLayout();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
spContainer.Children.Add(controlExtra);
}
}
Not sure if this is what you're asking. If not, please clarify paragraph 2.
Have a look at LogicalTreeHelper.GetChildren(myUiElement)
This looks at the logical tree rather than the visual tree so it examines the structure without needing to have loaded the control to get the visual structure
In the below control to find is the name of the contorl i.e. myDatagrid
You could also adapt this to just get all the children of a particular control i.e.
FindChildInVisualTree(this, "mydatagrid"); // assumming this a UIElement (i.e. your in the code behind)
find the control using the below then using LogicalTreeHelper get all it's children.
public static UIElement FindChildInVisualTree(UIElement view, string controlToFind)
{
UIElement control = null;
try
{
if (view != null)
{
if ((view as FrameworkElement).Name.ToUpper() == controlToFind.ToUpper())
{
control = view;
}
else
{
DependencyObject depObj = view as DependencyObject;
if (depObj != null)
{
foreach (var item in LogicalTreeHelper.GetChildren(depObj))
{
control = FindChildInVisualTree(item as UIElement, controlToFind);
if (control != null)
{
break;
}
}
}
}
}
}
catch (Exception ex)
{
throw new ApplicationException("Error finding child control: " + controlToFind, ex);
}
return control;
}

Binding a Textbox to a property in WPF

I have a Textbox in a User Control i'm trying to update from my main application but when I set the textbox.Text property it doesnt display the new value (even though textbos.Text contains the correct data). I am trying to bind my text box to a property to get around this but I dont know how, here is my code -
MainWindow.xaml.cs
outputPanel.Text = outputText;
OutputPanel.xaml
<TextBox x:Name="textbox"
AcceptsReturn="True"
ScrollViewer.VerticalScrollBarVisibility="Visible"
Text="{Binding <!--?????--> }"/> <!-- I want to bind this to the Text Propert in OutputPanel.xmal.cs -->
OutputPanel.xaml.cs
namespace Controls
{
public partial class OutputPanel : UserControl
{
private string text;
public TextBox Textbox
{
get {return textbox;}
}
public string Text
{
get { return text; }
set { text = value; }
}
public OutputPanel()
{
InitializeComponent();
Text = "test";
textbox.Text = Text;
}
}
}
You have to set a DataContext in some parent of the TextBox, for example:
<UserControl Name="panel" DataContext="{Binding ElementName=panel}">...
Then the binding will be:
Text="{Binding Text}"
And you shouldn't need this - referring to specific elements from code behind is usually bad practice:
public TextBox Textbox
{
get {return textbox;}
}
I hope this example will help you.
1) Create UserControl.
2) Add to XAML <TextBlock Text="{Binding Path=DataContext.HeaderText}"></TextBlock>
3) In the code behind of that UserControl add
public partial class MyUserControl: UserControl
{
public string HeaderText { set; get; } // Add this line
public MyUserControl()
{
InitializeComponent();
DataContext = this; // And add this line
}
}
4) Outside of the control and let's say in the MainWindow Load event you have to do like
this.gdMain = new MyUserControl{ HeaderText = "YES" };
If your are starting to bind properties I suggest you check some articles on MVVM.
This is a very powerful architecture you can use on WPF. I found it very useful in my projects.
Check this one.

WPF Editable ComboBox

I have a ComboBox and ComboBox.IsEditable property is set to True to have a ComboBox act as both a TextBox and a drop-down list simultaneously. But when the ComboBox is data-bound, entering custom text will not cause a new item to be added to the data-bound collection.
For example, if I enter 'Joe' in a ComboBox that is bound to a list of people, which doesn't contain the value 'Joe', then the value 'Joe' is not going to be added to the drop-down list automatically.
What is the best way to handle this?
Here's a basic MVVM compliant way of getting the behaviour you want:
MainWindow.xaml
<Window x:Class="WpfApplication1.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">
<StackPanel>
<ComboBox Margin="30,5,30,5"
IsEditable="True"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}"
Text="{Binding NewItem, UpdateSourceTrigger=LostFocus}"/>
<TextBox Margin="30,5,30,5" />
</StackPanel>
</Window>
MainWindow.cs
public partial class MainWindow : Window, INotifyPropertyChanged
{
private string _selectedItem;
private ObservableCollection<string> _items = new ObservableCollection<string>()
{
"One",
"Two",
"Three",
"Four",
"Five",
};
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
public IEnumerable Items
{
get { return _items; }
}
public string SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
public string NewItem
{
set
{
if (SelectedItem != null)
{
return;
}
if (!string.IsNullOrEmpty(value))
{
_items.Add(value);
SelectedItem = value;
}
}
}
protected void OnPropertyChanged(string propertyName)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
I had to place another control in the window as I have set the UpdateSourceTrigger property of the Text binding to LostFocus. If there are no other controls in the window then the Combobox will never lose focus.
I changed the update mode because the default update mode is Propertychanged which will add a new item for each keystroke.
E.G. if you entered the text "Window", the following would be added to your collection:
W
Wi
Win
Wind
Windo
Window
I would handle it in the LostFocus event.
Here you can check if the SelectedItem is null. If so, add the value of Text to the bound list and set SelectedItem to the new item.
In XAML:
<ComboBox Name="_list" LostFocus="LostFocus" IsEditable="True"/>
In code-behind:
private ObservableCollection<string> _names;
public MainWindow()
{
InitializeComponent();
_names = new ObservableCollection<string> {"Eric", "Phillip"};
_list.SetBinding(ItemsControl.ItemsSourceProperty, new Binding {Source = _names});
}
private void LostFocus(object sender, RoutedEventArgs e)
{
var comboBox = (ComboBox) sender;
if(comboBox.SelectedItem != null)
return;
var newItem = comboBox.Text;
_names.Add(newItem);
comboBox.SelectedItem = newItem;
}
Hope this helps :)
My suggestion would be using an MVVM-approach and bind your ComboBox.Text to some TextProperty of your ViewModel. (Same can be achieved by just adding a string property to your view)
Then you can treat the input in the setter of this property and add that new item to the list, no matter which way it was "committed" in the view.
AFAIK there is no out-of-the-box mechanism to add new items to your datasource, you will have to do the item-generation yourself anyway.
Alternatively, you can bind both - SelectedItem and Text of your ComboBox - to avoid lookups in case the user has entered a known item. But that part may be less important to answer your question.
I based this on Mr. Gale's answer. It uses a list of floats instead of strings and checks if the entered value has already been added to the list
private float zoomFactor = 2;
public float ZoomFactor
{
get { return zoomFactor; }
set
{
SetProperty(ref zoomFactor, value);
}
public float NewZoom
{
get { return zoomFactor; }
set
{
if (!zoomValues.Contains(value))
zoomValues.Add(value);
ZoomFactor = value;
}
}
private ObservableCollection<float> zoomValues = new ObservableCollection<float>()
{
1, 2, 3, 4
};
public ObservableCollection<float> ZoomValues { get => zoomValues; }
and the XAML:
<ComboBox
IsEditable="True"
ItemsSource="{Binding ZoomValues}"
SelectedItem="{Binding ZoomFactor}"
Text="{Binding NewZoom, UpdateSourceTrigger=LostFocus}"/>

Resources