WPF DataGrid, Template Column and Virtualization - c#

I have created VM classes for the DataGrid items (Rows and Cells).
The VM class for Cell is shown below:
public class ListGridCell : INotifyPropertyChanged
{
public ListGridCell(string Name)
{
// Init properties
this.Name = Name;
this.DataValue = null;
this.DataEditor = null;
}
public string Name { get; private set; }
private object _DataValue;
public object DataValue
{
get { return _DataValue; }
set { _DataValue = value; NotifyPropertyChanged("DataValue"); }
}
private FrameworkElement _DataEditor;
public FrameworkElement DataEditor
{
get { return _DataEditor; }
set { _DataEditor = value; NotifyPropertyChanged("DataEditor"); }
}
...
}
DataGrid columns and VM is built totally dynamically from code.
I create template column (DataGridTemplateColumn) and set the CellTemplate to the followind template (illustrated through XAML):
<StackPanel>
<TextBlock Text="{Binding Path=DataValue}" />
<ContentControl Content="{Binding Path=DataEditor}" />
</StackPanel>
Everything works fine and as expected when DataGrid is initially filled and shown.
Now, If I try to SCROLL the grid the following exception occurs:
Specified element is already the logical child of another element.
Disconnect it first.
This exception has something to do with grid's row virtualization and binding Content to DataEditor inside cell template. If I turn off the row virtualization, everything works fine but grid performance becomes very bad so it is not an option.
Do you know DataGrid virtualization works behind the scenes, what happens when row is loaded/unloaded and what could be causing this error?
Are there any workarounds?
Note: I cannot use ContentTemplate binding for cell data editor in cell template (which is suggested in many places as workaround) because I MUST manually create and initialize this cell editor.

Solved.
This is obviously a bug somewhere in ContentControl when using Content binding in combination with DataGrid virtualization.
Once I switched to ContentPresenter everything works flawlessly.
Working cell template is:
<StackPanel>
<TextBlock Text="{Binding Path=DataValue}" />
<ContentPresenter Content="{Binding Path=DataEditor}" />
</StackPanel>

Related

Datagrid SelectedIndex binding won't work in XAML

Question summary: Is there a way in XAML to ensure that my DataGrid component is fully loaded before it initiates a binding on the SelectedIndex property?
My ViewModel is set up like this. I'm using MVVM-light to notify the view of changes. I pass the new model to SetData() whenever the it gets updated from the server.
public class MyViewModel : ViewModelBase
{
public void SetData(DataModel model)
{
Data = model.Data; //Array of 75 DataObjects
DataIndex = model.Index; //int between 0 and 74
}
// Array to Bind to Datagrid ItemsSource
public DataObject[] Data
{
get { return _data; }
private set
{
if (_data!= value)
{
_data= value;
RaisePropertyChanged("Data");
}
}
}
private DataObject[] _data;
// Int to Bind to Datagrid SelectedIndex
public int DataIndex
{
get { return _index; }
private set
{
if (_index != value)
{
_index = value;
RaisePropertyChanged("DataIndex");
}
}
}
private int _index;
}
The view looks like this:
<Application.Resources>
<ResourceDictionary>
<core:ViewModelLocator x:Key="Locator" />
</ResourceDictionary>
</Application.Resources>
<DataGrid ItemsSource="{Binding MyViewModel.Data, Source={StaticResource Locator}}"
SelectedIndex="{Binding MyViewModel.DataIndex, Source={StaticResource Locator}, Mode=OneWay}"
AutoGenerateColumns="True"/>
My issue is that none of the rows are selected on my DataGrid. All the data shows up in the grid correctly but the row does not get selected. I've checked the properties and confirmed that the Array length is 75 and DataIndex is an int between 0 and 74.
It seems the reason is because the DataGrid hasn't finished loading when the binding is set. I can prove this by initializing the binding after the component is loaded. In this case, everything works as expected and my selected item is displayed correctly:
<DataGrid x:Name="MyDataGrid" Loaded="OnDataGridLoaded"
ItemsSource="{Binding MyViewModel.Data, Source={StaticResource Locator}}"
AutoGenerateColumns="True"/>
private void OnDataGridLoaded(object sender, RoutedEventArgs e)
{
Binding b = new Binding("DataIndex");
b.Source = Locator.MyViewModel.Data;
MyDataGrid.SetBinding(DataGrid.SelectedIndexProperty, b);
}
I'd prefer not to have to do it like this because, you know, code-behind. So is there a way to fix this using only XAML? Here is what I've tried so far (none of which worked for me):
Binding SelectedIndex to an int property on my ViewModel (as shown above)
Binding SelectedItem to a DataObject property on my ViewModel (same result)
Binding SelectedValue and SelectedPath to a property of my DataObject (This actually worked, for the first instance only. The problem is I have multiple instances of this Datagrid component, and for some reason this only works on the first instance)
Binding to an ObservableCollection instead of an Array (tried all 3 of the above methods with an ObservableCollection and got the same results for each)
Delaying the change notification by wrapping it in a call to Dispatcher.Invoke. This doesn't help because the component is not immediately in view.
Creating the binding in XAML and then updating the target in the Loaded function. MyDataGrid.GetBindingExpression(DataGrid.SelectedIndexProperty).UpdateTarget();
My original question was missing a piece of info that was causing the issue.
It seems there's a WPF bug when the binding source is a static link. If I move the DataIndex property into the component's DataContext, then it will work correctly.
I'm not going to do this however, because the data is shared between multiple instances. I don't need to have multiple instances of the data, only the component. Therefor, I will just right it off as a Microsoft bug, and use the code-behind work around.
I will leave the question open tho, in case anyone has a solution for this bug.

Setting ListView ItemsPanel Duplicates Items

I have a custom control in my application. One of the dependency properties is an ObservableCollection<ToggleButton>:
public ObservableCollection<ToggleButton> HeaderButtons
{
get { return (ObservableCollection<ToggleButton>)GetValue(HeaderButtonsProperty); }
set { SetValue(HeaderButtonsProperty, value); }
}
public static readonly DependencyProperty HeaderButtonsProperty = DependencyProperty.Register("HeaderButtons", typeof(ObservableCollection<ToggleButton>), typeof(Expandable), new PropertyMetadata(new ObservableCollection<ToggleButton>()));
I'm then putting them in a ListView in Generic.xaml:
<ListView ItemsSource="{TemplateBinding HeaderButtons}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
...and using it like this:
<controls:MyControl.HeaderButtons>
<ToggleButton x:Name="FilterButton">
<Image Source="/Assets/Icons/Empty Filter-512.png" Height="15" Width="15"/>
</ToggleButton>
</controls:MyControl.HeaderButtons>
However, I keep ending up with a duplicate item:
I can't figure out how that item is getting there. I can fix it by removing my custom ListView.ItemsPanel, but of course that makes my items flow vertically, defeating the entire purpose. Can anyone else see why this would be duplicating the item?
EDIT: For further interest, if I go into the Live Visual Tree I can see that both buttons have the name "FilterButton". Which should, of course, not be possible.
EDIT: Here's the ContentPresenter from the MainWindow:
<ContentPresenter Content="{Binding CurrentControl, Mode=OneWay}" Grid.Row="1" Grid.Column="1"/>
And CurrentControl is set to an instance of my UserControl:
private UserControl currentControl;
public UserControl CurrentControl
{
get { return currentControl; }
set
{
if (currentControl != value)
{
currentControl = value;
OnPropertyChanged("CurrentControl");
}
}
}
The source of the problem is the default value of your HeaderButtonsProperty - you set one using new PropertyMetadata(new ObservableCollection<ToggleButton>()). Contrary to what you expect it does not create one instance of the collection for each instance of your control, but a single instance shared across all of your controls.
Then you use this XAML syntax:
<controls:MyControl.HeaderButtons>
<ToggleButton (...) />
</controls:MyControl.HeaderButtons>
which does not assign a new collection to your HeadersButton property, but rather adds the specified item to the existing one. So each time this part of code is "executed", it adds a new copy of the ToggleButton to the single collection shared by all your controls.
To resolve the problem you should remove the default value from your HeaderButtonsProperty's metadata and assign a new collection instance in your control's constructor - that way each control instance will have it's own independent collection.

change textblock text that is inside Listbox in windowsphone 8

i want to change textblock text in page initialize event
here is my xaml
<ListBox Margin="3,60,1,10" BorderThickness="2" Grid.Row="1" Name="lstAnnouncement" Tap="lstAnnouncement_Tap" Width="476" d:LayoutOverrides="VerticalMargin">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Name="thispanel" Grid.Row="1" Orientation="Horizontal" Height="120" Width="478" >
<StackPanel.Background>
<ImageBrush ImageSource="Images/Text-ALU.png" Stretch="Fill" />
</StackPanel.Background>
<Grid HorizontalAlignment="Left" Width="30" Margin="0,0,0,2" Background="#FF0195D5" Height="118">
<TextBlock x:Name="txtDate" TextWrapping="Wrap">
</TextBlock>
</Grid>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
i want to change txtDate.Text using c# in code-behind but txtdate is not accessible in code behind so how to achieve it ?
The reason you're not able to access the txtDate object is because it's contained within the DataTemplate you're using for the ListBox. This isn't an error - the DataTemplate is being applied to every single item added to your ListBox.
Given that the ListBox creates, among other controls, a Grid containing a TextBlock with the name "txtDate", for every single item added to it, what would it mean to access the txtDate object? How would your program decide which of a (functionally) infinite number of txtDates associated with an identical number of ListBoxItems you meant when you referenced txtDate?
If you wanted to be able to easily change the content of txtDate, you'd want to bind the ItemsSource of your ListBox to a property in a ViewModel. The easiest way to do this would be to have that property be an IEnumerable containing a custom model type. This way, you could update the text property of that model and call NotifyPropertyChanged on the that property, and the UI would update to reflect the new data.
Here's an example:
public class YourViewModel
{
public List<YourModel> Models { get; set; }
}
public class YourModel : INotifyPropertyChanged
{
private string yourText;
public string YourText
{
get { return yourText; }
set
{
yourText = value;
NotifyPropertyChanged("YourText");
}
}
// add INotifyPropertyChanged implementation here
}
And then you'd want to bind the ItemsSource of the ListBox to YourViewModel's Models property, and the text of your TextBox to the YourModel's YourText property. Any time you change the YourModel.YourText property, it'll automatically update on the UI. I think it's probably subject to debate whether having your model implement INotifyPropertyChanged is proper MVVM, but I find it a lot easier in these cases than forcing the ViewModel to update every single model each time a change is made on one of them.
If you're not familiar with the MVVM pattern used with WPF, this might be a good start: MVVM example.
this function will help you... This will help u find the control inside of a listbox runtime..
public FrameworkElement SearchVisualTree(DependencyObject targetElement, string elementName)
{
FrameworkElement res = null;
var count = VisualTreeHelper.GetChildrenCount(targetElement);
if (count == 0)
return res;
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(targetElement, i);
if ((child as FrameworkElement).Name == elementName)
{
res = child as FrameworkElement;
return res;
}
else
{
res = SearchVisualTree(child, elementName);
if (res != null)
return res;
}
}
return res;
}
Here first parameter is parent and the second parameter is the name of the element which in your case is "txtDate".. hope it works!!

C# WPF Checkbox Databinding

I'm looking for the best way to populate a check boxes from the following code. I have looked into Binding but not really sure where to go.
Here is the edited code that is working
private void dpDateSelection_SelectedDateChanged(object sender, SelectionChangedEventArgs e)
{
DateTime? date = dpDateSelection.SelectedDate;
logDate = date != null ? date.Value.ToString("yyyy-MM-dd") : null;
dpDateSelection.ToolTip = logDate;
LoadLogs(logDate);
}
private void LoadLogs(string ldate)
{
string[] logs = Directory.GetFiles(logPath + ldate, "*.ininlog");
InitializeComponent();
logList = new ObservableCollection<String>();
logList.Clear();
foreach (string logName in logs)
{
string s = logName.Substring(logName.IndexOf(ldate) + ldate.Length + 1);
int extPos = s.LastIndexOf(".");
s = s.Substring(0, extPos);
logList.Add(s);
}
this.DataContext = this;
}
<ListBox x:Name="Logs" ItemsSource="{Binding logList}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding}" ToolTip="{Binding}" Tag="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You will want to start by using an ItemsControl instead of a StackPanel, since ItemsControls are automatically set up to display collections of things:
<ItemsControl ItemsSource="{Binding Logs}"/>
Note the use of ItemsSource. With the accompanying binding string, it basically says "Look for a property on the DataContext called "Logs" and put everything in it into this control".
Next you said you wanted this displayed as checkboxes, so we use an item template:
<ItemsControl ItemsSource="{Binding Logs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Content={Binding .}/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This says "Use a checkbox for each Item in the ItemsSource". The DataTemplate can be a Grid or other collection control as well, so this is a really powerful feature in WPF. The "Binding ." just binds to the object itself (a string in this case, so we don't need a special path).
Finally, you need to set up a property to bind to in your view model:
ObservableCollection<String> Logs {get; set;}
You want an ObservableCollection so that when anything is added to or removed from the list, it automatically updates the UI. If you are going to be completely replacing the list (assignment), then you need to implement INotifyPropertyChanged and invoke the PropertyChanged event in that properties setter.
In your posted loop, you would add each log file to this property.
Also, make sure that somewhere you set the DataContext property of the XAML file (View) to your view model object. If everything is in code behind, use DataContext = this. Note that doing this is considered bad practice, and you should use a separate class (ViewModel).
You didn't mention what you wanted the CheckBoxes to do, so I haven't included anything related to that in my answer. You will likely want to abstract your logs into an object with a "Selected" property you can then bind the IsChecked property of the CheckBoxes to.
Obviously this is a lot to take in, please let me know if I can clarify anything or help further!
Update
You put the property in your ViewModel (DataContext). Whatever class that is, you write:
ObservableCollection<String> Logs {get; set;}
private void LoadLogs()
{
string[] logs = Directory.GetFiles(logPath + logDate, "*.ininlog");
foreach(string logName in logs)
{
string s = logName.Substring(logName.IndexOf(logDate) + logDate.Length + 1);
int extPos = s.LastIndexOf(".");
s = s.Substring(0, extPos);
//MessageBox.Show(s);
Logs.Add(s); //Add the parsed file name to the list
}
}

WPF lookupedit binding not woking

I am trying to bind the result from WCF service to devexpress lookupedit.
this is the property I created
<!-- language: c# -->
public class BindingModel
{
private static List < VW_ClientProcess> _clientProcess= new List< VW_ClientProcess>();
public List< VW_ClientProcess> clientProcess
{
get
{
return _clientProcess;
}
set
{
_clientProcess = value;
OnPropertyChanged("clientProcess");
}
}
}
}
In WPFApp.xaml.cs
BindingModel bind=new BindingModel();
bind.clientProcess = e.Result.GetClientProcessesResult.ToList< VW_ClientProcess>();
this is my xaml code(WPFApp.xaml)
<dxlc:LayoutGroup>
<dxlc:LayoutGroup.DataContext>
<ViewModel:BindingModel />
</dxlc:LayoutGroup.DataContext>
<dxlc:LayoutItem x:Name="liClientProcess"
Width="auto"
VerticalAlignment="Bottom"
Label="Client Process"
LabelPosition="Top">
<dxg:LookUpEdit x:Name="lueClientProcess"
AutoPopulateColumns="True"
DisplayMember="ClientFullName"
ItemsSource="{Binding clientProcess}"
ValueMember="ProcessID" />
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
The problem is when I set ItemSource in xaml, only column names are displaying but the data fields are empty.
but If I set ItemSource through c# code like this
BindingModel bind = new BindingModel();
lueClientProcess.ItemsSource = bind.clientProcess;
lookupedit edit is getting populated. I am new to WPF. I don't know what I am doing wrong here.
It looks like the problem is you are creating duplicate instances of BindingModel. So here in the XAML creates one instance, and assigns it to the view's DataContext:
<dxlc:LayoutGroup.DataContext>
<ViewModel:BindingModel />
</dxlc:LayoutGroup.DataContext>
But then this doesn't use the existing instance, but creates a new one that isn't attached anywhere to the UI:
BindingModel bind=new BindingModel();
bind.clientProcess = e.Result.GetClientProcessesResult.ToList< VW_ClientProcess>();
So I guess what you'd want, instead of the above:
var bind = (BindingModel)DataContext;
bind.clientProcess = e.Result.GetClientProcessesResult.ToList< VW_ClientProcess>();

Categories

Resources