Binding to DataGridTemplateColumn in WPF DataGrid - c#

I am customizing WPF DataGrid in a generic way. consider that I have defined an enum of Creatures like this:
public enum CreatureType
{
Human,
Animal,
Insect,
Virus
}
then I have an attached property of the above type so it can be attached to any control I want, that called CreatureTypeProeprty. then I want to attach this property to DataGridTemplateColumn and I want to access this inside the Template. for example consider the following reproducible code of my problem:
<DataGridTemplateColumn local:CreatureTypeProeprty.Value = "Human" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=(local:CreatureTypeProeprty.Value), RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGridTemplateColumn }}"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
This doesn't work and I know why because DataGrid column is not part of the visual tree. but I can't find any way to solve this issue. I also tried This but this solution can not be used in a generic way and ends up in a very dirty solution! I'm looking for a more sophisticated and clean way. I also tried using the name for the column and specifying ElementName it works but also this can not be used when there is multiple columns and I have to repeat codes every time.

The datagrid column is more a conceptual thing. It's not so much not in the visual tree as not there at all.
There is no spoon.
One way to do this sort of thing would be to use a list and index it. In your window viewmode
public list<CreatureType> Creatures {get;set;}
Obviously, load that up with CreatureTypes in the same order as your columns.
You can then reference using something like
Text="{Binding DataContext.Creatures[4]}
, RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=Datagrid}}
Unfortunately, you need a fixed number for indexing.
You could perhaps dynamically build your columns and add them to the datagrid.
I would do that by defining a string template and substituting values, then using xamlreader.parse() to build a datagrid column which you add to your datagrid.
In my sample I have a col.txt file. This is a sort of template for my datagrid columns.
It is pretty much a piece of datagrid column markup.
<?xml version="1.0" encoding="utf-8" ?>
<DataGridTemplateColumn>
<DataGridTemplateColumn.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="xxMMMxx" TextAlignment="Center" Grid.ColumnSpan="2"/>
<TextBlock Text="Units" Margin="2,0,2,0" Grid.Row="1"/>
<TextBlock Text="Value" Margin="2,0,2,0" Grid.Row="1" Grid.Column="2" />
</Grid>
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding MonthTotals[xxNumxx].Products}" Margin="2,0,2,0" TextAlignment="Right"/>
<TextBlock Text="{Binding MonthTotals[xxNumxx].Total}" Margin="2,0,2,0" TextAlignment="Right" Grid.Column="1"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
It's showing totals for months so obviously 12 months and my array here is monthtotals[12].
Obviously, this is a different thing and illustrating the approach.
I also have a dg.txt file which is of course something rather similar for my datagrid. But that'd be repetitive.
And I have some code substitutes strings, parses the result and appends a built column:
private void Button_Click(object sender, RoutedEventArgs e)
{
// Get the datagrid shell
XElement xdg = GetXElement(#"pack://application:,,,/dg.txt");
XElement cols = xdg.Descendants().First(); // Column list
// Get the column template
XElement col = GetXElement(#"pack://application:,,,/col.txt");
DateTime mnth = DateTime.Now.AddMonths(-6);
for (int i = 0; i < 6; i++)
{
DateTime dat = mnth.AddMonths(i);
XElement el = new XElement(col);
// Month in mmm format in header
var mnthEl = el.Descendants("TextBlock")
.Single(x => x.Attribute("Text").Value.ToString() == "xxMMMxx");
mnthEl.SetAttributeValue("Text", dat.ToString("MMM"));
string monthNo = dat.AddMonths(-1).Month.ToString();
// Month as index for the product
var prodEl = el.Descendants("TextBlock")
.Single(x => x.Attribute("Text").Value == "{Binding MonthTotals[xxNumxx].Products}");
prodEl.SetAttributeValue("Text",
"{Binding MonthTotals[" + monthNo + "].Products}");
// Month as index for the total
var prodTot = el.Descendants("TextBlock")
.Single(x => x.Attribute("Text").Value == "{Binding MonthTotals[xxNumxx].Total}");
prodTot.SetAttributeValue("Text",
"{Binding MonthTotals[" + monthNo + "].Total}");
cols.Add(el);
}
string dgString = xdg.ToString();
ParserContext context = new ParserContext();
context.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
context.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
DataGrid dg = (DataGrid)XamlReader.Parse(dgString, context);
Root.Children.Add(dg);
}
private XElement GetXElement(string uri)
{
XDocument xmlDoc = new XDocument();
var xmltxt = Application.GetContentStream(new Uri(uri));
string elfull = new StreamReader(xmltxt.Stream).ReadToEnd();
xmlDoc = XDocument.Parse(elfull);
return xmlDoc.Root;
}
Might be more complicated than you need but maybe it'll help someone else down the line.
BTW
I suggest an "unknown" or "none" as the first in any enum. This is standard practice in my experience so you can differentiate between a set and unset value.
Q1 How common is this?
Not very. It solves problems I would describe as "awkward" bindings and can simplify UI logic in templating.
Or a sort of super auto generator for columns.
A similar approach can be very useful for user configurable UI or dynamic UI.
Imagine a dynamic UI where the user is working with xml and xml defines the UI. Variable stats or variable points calculation for a tabletop wargaming points calculator.
Or where the user picks the columns they want and how to display them. That can be built as strings and saved to their roaming profile. Loaded from there using xamlreader.load which gives you your entire datagrid definition.
q2
That's what the sample does.
dg.txt looks like
<?xml version="1.0" encoding="utf-8" ?>
<DataGrid
ItemsSource="{Binding SalesMen}"
AutoGenerateColumns="False"
Grid.Column="1"
CanUserAddRows="False"
IsReadOnly="False"
Name="dg">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding FullName}" Header="Name"/>
</DataGrid.Columns>
</DataGrid>
This line gets Columns
XElement cols = xdg.Descendants().First(); // Column list
And this line append each column
cols.Add(el);
It is so there are Xelement nodes you can find that the file is manipulated as xml rather than just as a string.

Related

.NET MAUI: Scrollable list of log data

I'm making an application for gathering serial communication data and presenting it in a log window. This is my first .NET MAUI application, and I'm having problems with finding a way that performs well.
Preferably I would like to have columns with timestamp, raw hex bytes and ascii strings. But using a ListView with a Grid for each line with perhaps a couple hundred lines is not performing very well even on my Macbook M1 Max. It's really sluggish and completely unusable.
<ListView
ItemsSource="{Binding ReceivedDataBuffer}"
ios:ListView.RowAnimationsEnabled="False"
HasUnevenRows="False"
CachingStrategy="RetainElement">
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:ReceivedData">
<ViewCell>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Text="{Binding Timestamp}"
FontAttributes="Bold" />
<Label Grid.Column="1"
Text="{Binding HexString}"
FontAttributes="Italic"/>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Is there a way to get the ListView to perform better? Or is there a control that's better suited to show a lot of logged data (approx. 10,000 lines) with multiple properties?
You can use the DataGrid instead of ListView if you want to perform well. As the DataGrid can show different data types in different types of columns and it will only load what you scroll to.
I use the code below to show 10,000 lines of pseudo data in DataGrid and it performs well.
for(int i = 0; i<10000; i++)
{
orderInfo.Add(new OrderInfo("testdata column"+i, "testdata column" + i, "testdata column" + i, "testdata column" + i, "testdata column" + i));
}
And you can refer to DataGrid in .NET MAUI on how to use it.

WPF ordering XAML elements in StackPanel at runtime based on a config file

I'm developing a WPF app using MVVM pattern with Caliburn.Micro
I have a config file that contains positions of where XAML elements should be inside of a StackPanel
# in this case RU_ELEMENT should be at the top, EN_ELEMENT second and DE_ELEMENT last
EN_ELEMENT = 1
DE_ELEMENT = 2
RU_ELEMENT = 0
This seems to be pretty basic yet I'm unable to find a way to do this. I found this thread: change the children index of stackpanel in wpf but changing it this way seems to be too complicated for what I am after. I just need to set an index of an element from a variable. I feel like there should be a much simpler way. I'm also ok with using some other, perhaps more appropriate layout panel than StackPanel.
XAML:
<!-- Language1 -->
<TextBlock Text="English" Foreground="DarkGray" FontSize="16"/>
<TextBox
VerticalAlignment="Top"
Height="150"
Text="{Binding SelectedItem.ValueEN, UpdateSourceTrigger=PropertyChanged}"
cm:Message.Attach="[Event GotFocus] = [Action FocusedTextBox('english')]" />
<!-- Language2 -->
<TextBlock Text="German" Foreground="DarkGray" FontSize="16"/>
<TextBox
VerticalAlignment="Top"
Height="150"
Text="{Binding SelectedItem.ValueDE, UpdateSourceTrigger=PropertyChanged}"
cm:Message.Attach="[Event GotFocus] = [Action FocusedTextBox('german')]" />
On a side note: I find WPF and C# in general to have much less discussions and "how to" guides than all of my previous languages (Java, Python, JS) so researching things online is usually a dead end for me. I'm not sure to why that is since C# is a very popular language but I'm really struggling with finding help online.
A solution could be to use an ItemsControl that would host the xaml elements. You can bind the items like <ItemsControl ItemsSource="{Binding ListOfItems} ...
Then you could easily sort the items in the corresponding ViewModel. Like so:
public BindableCollection<YourElement> ListOfItems {get;set;}
...
ListOfItems.Sort()
Note that YourElement class should have a comparator.
EDIT: As per request I'll explain it more detailed:
In your Xaml you have to declare a ItemsControl like so:
<ItemsControl ItemsSource="{Binding ListOfItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Language}" Foreground="DarkGray" FontSize="16"/>
<TextBox
VerticalAlignment="Top"
Height="150"
Text="{Binding TextValue, UpdateSourceTrigger=PropertyChanged}"
cm:Message.Attach="[Event GotFocus] = [Action FocusedTextBox($this)]" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
And in your backend you should first create a class that's going to represent your item in the ItemsControl. For example:
public Class MyItem{
public string Language {get;set;}
public string TextValue {get;set;}
}
Finally in your ViewModel you'll need to create the list of items that you bind with the ItemsControl like so:
public BindableCollection<MyItem> ListOfItems {get;set;}= new BindableCollection<MyItem>();
//here you can add them in the order that is specified by the config file
public void LoadItems(){
ListOfItems.Add(new MyItem{Language="English"});
ListOfItems.Add(new MyItem{Language="Russian"});
ListOfItems.Add(new MyItem{Language="German"});
}
public void FocusedTextBox(MyItem item){
//do here whatever you want
}

Special Sorting ListView when clicking header

I got a row like this:
XAML:
<ListView x:Name="ListViewAnlagen"
Grid.RowSpan="2"
ItemContainerStyle="{StaticResource TempContainerStyle}"
VerticalAlignment="Top" HorizontalAlignment="Left"
Height="571" Width="1314"
Margin="0,53,0,0"
AlternationCount="2"
GridViewColumnHeader.Click="GridViewColumnHeaderClickedHandler">
<ListView.View>
<GridView ColumnHeaderContainerStyle="{DynamicResource CustomHeaderStyle}">
<GridView.Columns>
<GridViewColumn Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border x:Name="border"
BorderBrush="Gray" BorderThickness=".5" Margin="-6,-3">
<TextBlock Text="{Binding EqNr}" Margin="6,3"/>
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="EQ Nr."/>
<Image Source="img/filter.png"
Width="20" Height="20" Margin="25 0 0 0"
MouseDown="Image_MouseDown_1" />
</StackPanel>
</GridViewColumn>
I have added a click handler: GridViewColumnHeader.Click
My Question is, how to sort this ascending and descending. I already looked at some others solutions, but it seems they only work when you bind it with DisplayMemberBinding.
What I already tried:
this
Since you already examined the example as commented by #AmolBavannavar (https://code.msdn.microsoft.com/windowsdesktop/Sorting-a-WPF-ListView-by-209a7d45), here is a hybris between the example and your current approach.
The main obstacle in adapting the example is the usage of GridViewColumnHeader.Command and GridViewColumnHeader.CommandParameter. Your equivalent for the command is the GridViewColumnHeader.Click="GridViewColumnHeaderClickedHandler", but you still need an equivalent to the command parameter.
I suggest you create an attached string property for this purpose and use it to attach the sort property name to the GridViewColumn. For the sake of demonstration, I don't create a new property but instead misuse the TextSearch.TextPath attached property:
<GridViewColumn Width="100" TextSearch.TextPath="EqNr">
Note that the "EqNr" is the same as the property name that is used for binding inside the cell template later.
Now, everything is in place to be used inside the click handler.
Get the clicked column header
Get the associated column
Get the attached property value that contains the sort property name
Get the collection view that is associated with the items source (or items)
Change the sort descriptions of the collection view
Code with simplified sorting logic:
private void GridViewColumnHeaderClickedHandler(object sender, RoutedEventArgs e)
{
var h = e.OriginalSource as GridViewColumnHeader;
if (h != null)
{
var propertyName = h.Column.GetValue(TextSearch.TextPathProperty) as string;
var cvs = ListViewAnlagen.ItemsSource as ICollectionView ??
CollectionViewSource.GetDefaultView(ListViewAnlagen.ItemsSource) ??
ListViewAnlagen.Items;
if (cvs != null)
{
cvs.SortDescriptions.Clear();
cvs.SortDescriptions.Add(new SortDescription(propertyName, ListSortDirection.Descending));
}
}
}
Note that for the sake of demonstration I only clear the sort descriptions and add a static descending sort description. For your actual application, you may want to keep track (or analyze) the current sorting status for the column and then alternate between ascending and descending sort.

Difficulty updating UI when contents of ObservableCollection is changed?

I have a combobox bound to an observablecollection as:
<ComboBox
HorizontalAlignment="Left" Margin="586,51,0,0" VerticalAlignment="Top" Width="372"
SelectedItem="{Binding PrimaryInsurance.SelectedInsurance}"
ItemsSource="{Binding PrimaryInsurance.AllPatientInsurance}"
ItemTemplate="{StaticResource InsuranceTemplate}" />
The observablecollection itself is defined as:
private ObservableCollection<Insurance> _allPatientInsurance;
public ObservableCollection<Insurance> AllPatientInsurance
{
get { return _allPatientInsurance; }
set { if (_allPatientInsurance == value) return; _allPatientInsurance = value; OnPropertyChanged("AllPatientInsurance"); }
}
Now Insurance encapsulates data downloaded from the database an adds INotifyPropertyChanged as:
public string CompanyName
{
get { return insurance_View.Companyname; }
set { if (insurance_View.Companyname == value) return; insurance_View.Companyname = value; OnPropertyChanged("CompanyName"); }
}
Where insurance_View is the raw data record downloaded from the database.
All is good.
However, in an "UnDo" operation, I would like to replace the edited insurance_View record with its original record like:
internal void UnDo()
{
insurance_View = (Insurance_View)pre_edit_Insurance_View.Clone();
}
Although the edited version of insurance_View is correctly changed back to its original form, the display is not being updated. Futhermore, replacing the edited version of insurance with the original version of insurance in the ObservableCollection like:
AllPatientInsurance.Remove(Old Insurance);
AllPatientInsurance.Add(New Insurance);
Destroys all the bindings and displays a blank record.
So, what is the best way to update the display when the contents of Insurance is changed without destroying the Insurance Object? Is there a better way?
Edit #1. Just to be clear, I am trying to replace the data record within the Insurance object where it is the Insurance object that is bound to the display. I am not wanting to replace the entire collection being displayed. The only thing that comes to mind is to replace each value of the edited record with its original value, but this seems tedious so I am hoping for a better method.
Edit #2. Is there any way to trigger the Insurance setters when the encapsulated
Insurance_View record is changed?
Edit #3. The insurance template:
<!--DataTemplate for the Combobox Insurance List-->
<DataTemplate x:Key="InsuranceTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20" />
<ColumnDefinition Width="120" />
<ColumnDefinition Width="50" />
<ColumnDefinition Width="14" />
<ColumnDefinition Width="50" />
<ColumnDefinition Width="60" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding XNotInEffect}" Grid.Column="0" />
<TextBlock Text="{Binding CompanyName}" Grid.Column="1"/>
<TextBlock Text="{Binding EffectiveDateFrom}" Grid.Column="2"/>
<TextBlock Text="--" Grid.Column="3" />
<TextBlock Text="{Binding EffectiveDateTo}" Grid.Column="4" />
<TextBlock Text="{Binding InsuranceType}" Grid.Column="5"/>
</Grid>
</DataTemplate>
Also, please note that simply removing then adding the same Insurance object results in its disappearance from the combobox drop down. Example:
AllPatientInsurance.Remove(SelectedInsurance);
AllPatientInsurance.Add(SelectedInsurance);
TIA
I suppose your InsuranceTemplate has bindings to the Insurance' properties such as CompanyName and therefore listens to property changed events from the Insurance instance (the template's DataContext). So because the undo operation doesn't change the properties by calling the corresponding setters (but by changing the insurance_view) you have to manually trigger the property changed events (OnPropertyChanged("CompanyName") and so on) of every changed property after the undo operation to notify the view.
Track the old insurance in its own observable collection. In your undo, you can assign the old collection to AllPatientInsurance, and let the property do the heavy lifting.
//initialize this elsewhere as appropriate
private ObservableCollection<Insurance> _oldPatientInsurance;
internal void UnDo()
{
insurance_View = (Insurance_View)pre_edit_Insurance_View.Clone();
AllPatientInsurance = _oldPatientInsurance;
}
This code does not work because the SelectedInsurance becomes null the moment it is removed from the collection:
AllPatientInsurance.Remove(SelectedInsurance);
AllPatientInsurance.Add(SelectedInsurance);
However, if a reference to the SelectedInsurance is kept then it can be added back as so:
SelectedInsurance.Reset();
var x = SelectedInsurance;
AllPatientInsurance.Remove(SelectedInsurance);
AllPatientInsurance.Add(x);
SelectedInsurance = x;
Where Reset() is
internal void Reset()
{
insurance_View = (Insurance_View)pre_edit_Insurance_View.Clone();
}
and the last line sets the combobox back to the initially selected item.
So simple. :)

DataGrid CellTemplate TabNavigation

I assume this has been asked before but I couldn't find it, so let me know if it's a duplicate found with other verbiage or something.
The problem is with an SL4 DataGrid which contains multiple CellTemplate's including Checkbox, Button etc. By default it will only tab through these elements on the first row. If I set TabNavigation="Cycle" it will tab through all the elements, but it doesn't move on to the next elements and instead just re-iterates the tabbing through the same DataGrid.
If I set it to Once then again it will only tab through the first row....and SL4 doesn't appear to have a Continue option to move onto the next object once it reaches the edge.
I'm looking for just an easy way to take the equivalent of TabNavigation="Cycle" except when it reaches the last tab-able element in the DataGrid then it moves on to the next thing in the tree instead of just tabbing back to the first element in the DataGrid again. What am I missing here?
There doesn't seem to be a native way to do this in Silverlight, here is a list of supported key strokes in the data grid control: http://msdn.microsoft.com/en-us/library/cc838112(v=vs.95).aspx
I was able to fake it by using a KeyDown event and checking for Tab then setting the editing cell manually:
<Grid x:Name="LayoutRoot" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox Grid.Row="0" Text="Some text" />
<sdk:DataGrid Grid.Row="1" ItemsSource="{Binding People}" AutoGenerateColumns="False" KeyDown="DataGrid_KeyDown">
<sdk:DataGrid.Columns>
<sdk:DataGridTemplateColumn>
<sdk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding FirstName}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellEditingTemplate>
</sdk:DataGridTemplateColumn>
<sdk:DataGridTemplateColumn>
<sdk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding LastName}" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellEditingTemplate>
</sdk:DataGridTemplateColumn>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
<TextBox Grid.Row="2" Text="Some more text" />
</Grid>
private void DataGrid_KeyDown(object sender, KeyEventArgs e)
{
DataGrid dg = (DataGrid)sender;
ObservableCollection<Person> items = dg.ItemsSource as ObservableCollection<Person>;
if (e.Key == Key.Tab && dg.SelectedIndex < items.Count -1)
{
dg.SelectedIndex++;
dg.CurrentColumn = dg.Columns[0];
dg.BeginEdit();
var cell = dg.CurrentColumn.GetCellContent(dg.SelectedItem);
}
}
I had some exp wth SL4 long time back. I will give your problem a try:
See the property you set to get your desired behavior won't work. It will be the Microsoft way only, thus alternative is to write your own code to achieve the required behavior.
My idea is to attach the following event to each datagrid cell:
private void DataGridCell_KeyDown(object sender, KeyEventArgs e)
{
if (keypressed == 'TAB' && last cell of the datagrid)
{
e.handled=true;
int tabIndex = dg.TabIndex;
tabindex++;
Control control = GetControl(tabIndex); // You can use visual tree in the method to get it
control.select();
control.focus();
}
}
My apologies I have written pseudo instead of real code, since it will take time for me to recall the code I use to do in SL.
Hope this solution works for you both way, when you tab out of data-grid and reverse tab in to the data-grid.

Categories

Resources