Can WPF Data Bind Without Going Through a Windows Control? - c#

This will take some explaining.
I'm writing a tool in WPF / C# to dynamically generate, in the view, a visual graph of the data in the view-model. The top-most parent is a grid, and each horizontal row of data is a canvas (inside a border). The canvas holds all the other UI elements (like TextBlocks).
I have a class to hold each row of UI elements in the view.xaml.cs, defined like this:
class ReportRow
{
public Border Divider;
public Canvas Row;
public TextBlock Title;
public List<TextBlock> Phases = new List<TextBlock>();
}
Then I define the entire graph as a List of these Rows:
List<ReportRow> reportRows = new List<ReportRow>();
In the viewmodel.cs, I have the data listed as an ObservableCollection so I can data bind to it and access the data from the view:
public ObservableCollection<SDDeliverable> Deliverables
{
get
{
return this.deliverables;
}
private set
{
this.deliverables = value;
this.RaisePropertyChanged(() => this.Deliverables);
}
}
Back in the view, I loop through the ObservableCollection, creating the rows and assigning the data to the elements (shown without all the styling and positioning, for brevity):
reportRows.Add(new ReportRow());
reportRows[i].Divider = new Border();
ProjectDisplay.Children.Add(reportRows[i].Divider);
reportRows[i].Row = new Canvas();
reportRows[i].Divider.Child = reportRows[i].Row;
reportRows[i].Title = new TextBlock();
reportRows[i].Title.SetBinding(TextBlock.TextProperty, new Binding(string.Format("Deliverables[{0}].DeliverableTitle", i)));
reportRows[i].Row.Children.Add(reportRows[i].Title);
Now, my original problem was that, because I'm binding each individual member of the collection (rather than binding the whole collection to one UI element, like a ListView), the view has no idea how long the collection is, which means I can't use a "foreach" or a loop counter. It worked fine with an arbitrary number of rows, but I didn't want to have to guess.
What I did was add a new label to the UI, bound to the length of the collection, and disguised it as a bit of helpful info:
<Label x:Name="DeliverableCountLabel" Content="{Binding Path=DeliverableCount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
this.DeliverableCount = Deliverables.Count;
Once I got that number into a view control, I was able to use it as my loop counter:
int totalItems;
if (int.TryParse(DeliverableCountLabel.Content.ToString(), out totalItems))
{
for (int i = 0; i < totalItems; i += 1)
{
reportRows.Add(new ReportRow());
This is a hacky fix, but I was alright with using it once.
The new problem is that once wasn't enough. As I add more data to the model, I'm hitting the same problem. Each Deliverable has a list of Phases and each Phase has a list of Tasks. I don't want to clutter up the UI with number labels all over the place.
I feel like there should be a way to use a data binding without having it go through the xaml or a visual control element. I just want to bind a variable in the view to a variable in the view-model so I can look at certain bits of info that I don't necessarily want to show the user.
I started messing with doing it this way:
Binding testBinding = new Binding("DeliverableCount");
However, it's the next step that's confounding me. Everything I've tried past that point has been incorrect somehow.
// returns the binding object itself, not the bound value
testBinding.ToString();
// error (not a real thing you can do, apparently)
string testString;
testString.SetBinding (testBinding);
How do I send a value from view-model to view without having to display it on-screen somewhere? Am I going about this the wrong way? Is this even possible?
A last-ditch idea I have is to create one dummy label and either make it invisible somehow or hide it behind another element. Then I could write a function to update the data binding on this one specific label any time I needed to access something in the view-model that's not shown on-screen. However, this really feels like a hack of a hack and I'd rather not go down that road unless it's really the best (or only) option.

This is how I ended up solving this. It's hacky, but it works.
I created a label in the xaml and set it's visibility to hidden. Then I just call one of these functions:
public string TempStringBind(string bind)
{
DummyLabel.SetBinding(Label.ContentProperty, new Binding(bind));
return DummyLabel.Content.ToString();
}
public int TempIntBind(string bind)
{
DummyLabel.SetBinding(Label.ContentProperty, new Binding(bind));
int newInt;
if (DummyLabel.Content != null && int.TryParse(DummyLabel.Content.ToString(), out newInt))
{
return newInt;
}
else
{
return -1;
}
}
This will take any variable from the view-model that can be bound to, bind it to an invisible label, grab that value from the label, and return it to me in the view in a useable form. While it's still going through something in the view xaml, the benefit is that the user doesn't have to see a bunch of extra controls or data they don't care about just to find out how many rows or columns I need to make for lists.
The ItemsSource method would be a lot cleaner, but that only works if I'm sticking the data in an existing control, like a ListView or a ComboBox, which aren't good for making visual charts and graphs with exact positioning.
I'm not sure what you guys meant by it not being MVVM. I've got the M, the V, and the VM all in there. :P

Related

How can I create a DataGrid with dynamic columns and cell templates?

I liked this approach to the solution, except that it will not work with virtualization.
Here's the relevant code from that post, abbreviated:
protected override FrameworkElement GenerateElement(DataGridCell cell, object dataItem)
{
DataRowView dataRow = (dataItem as DataRowView);
object cellData = dataRow[cell.Column.DisplayIndex];
var contentHost = new ContentControl() { Content = cellData };
contentHost.ContentTemplate = (DataTemplate)SomeResourceDictionary["SomeResourceKey"];
return contentHost;
}
This method is called by the DataGrid for every cell created/rendered. If virtualization is enabled, then the FrameworkElement created here may be recycled, i.e. used again.
The problem: using the information stored in cell and dataItem the content is determined and a direct reference is passed to the created ContentControl. If this FrameworkElement is reused, it will reference the same content - regardlesss of which row it is being rendered for.
Result: during (quick) scrolling the content displayed in the grid rows will randomly be mixed up.
My question: using cell.Column.DisplayIndex can the ContentControl.Content binding be set without including a direct reference to the desired content? Maybe, as if I defined it in Xaml something like this: "{Binding Path=dataRow[0]}" - but that also doesn't seem quite right.
Any suggestions greatly appreciated!
#BionicCode:
There are two challenges addressed here:
Binding a DataGrid to a source with dynamic columns.
Allow a DataTemplate to be defined for each dynamically generated column.
The first issue is covered quite well in the post linked above: create a DataTable from the original source and bind the DataGrid to that. The difficult part of the second issue - and why DataGridTemplateColumns cannot simply be used - is that DataGridTemplateColumns receive the entire row item as data context, i.e. to access the correct data, the template would have to know which column it is being used in.
This is where the code solution shown above comes in, by manually generating the FrameworkElement and "statically" assigning the desired content.
Luckily, the fix is rather simple, see my answer below.
I knew the solution had to be close and, luckily, it was. In the code above replace the following line...
var contentHost = new ContentControl() { Content = cellData };
...with these:
var contentHost = new ContentControl();
BindingOperations.SetBinding(contentHost, ContentControl.ContentProperty, new Binding("[" + cell.Column.DisplayIndex + "]"));
Now the ContentControl can dynamically bind to the correct column and virtualization works. At least EnableRowVirtualization, I haven't tested EnableColumnVirtualization, yet.

Update DataGrid on run-time

I am creating Windows Application which has a Grid.
Grid's data source is a an Object of a Class.
Grid has two bands.
As shown in the below image on Band1, there is a Column named as Templates.
It has values from 1 to 10.
The requirement is that based on the selected values in the Template Field, Band2 must have no of rows.
e.g. if User selects 2 in the Template Field, Band2 must have two rows.
As its a run time process, Grid must be refreshed on the Run time.
In the below case if the value is changed from 2 to 3 in the template field, the open Band2 must be refreshed to show 3 rows.
I wrote some code but it was not able to refresh the grid on the run time.
private void grdDataMapping_AfterCellUpdate(object sender, CellEventArgs e)
{
if (e.Cell.Column.Key.Equals("TemplateName"))
{
ValueList paramName = new ValueList();
string templateName = e.Cell.Text;
List<TemplateMapping> tempMappings = new List<TemplateMapping>();
if (_dictTemplateNames.ContainsKey(templateName))
{
for (int i = 0; i < templateName.Value; i++)
tempMappings.Add(new TemplateMapping());
mappingDetails.ListTemplateMapping = tempMappings;
}
grdDataMapping.Refresh();
}
What am I missing here?
You could implement the interface INotifyPropertyChanged and some additional properties you may notify of. This way the ViewModel notifies the View (or vice versa) about any changes that have been made. Thus, you can easily control the content of all 3 DataGrids.
See INotifyPropertyChanged for more on this. There are plenty of articles around who help you archiving this.

WPF MVVM Custom Validation : Need to show red border row based on Sort Test Pass on a particular column in DataGrid

Here is the situation. I am using Commands to bind to Execution logic, but I need to validate if the particular column of the DataGrid is sorted, throw a popup alert to user and based on his choice (Resort: Yes/No?) either resort or highlight the DataGrid rows (which are out of order).
Suggest me how to implement this. Stating clearly whether it should be in view-model or codebehind.
Scenario:My dataGrid ItemSource is bound to an Observable collection of my viewModel and i am checking the date column for sorting
VIEW MODEL Based approach
I have a ViewModel as shown with the following command
public void Calculate()
{
//Need to pass instance of DataGRid to manipulate
//do something
if (GridValidations.CheckValidationOnCalculate(this))
{
}
}
CheckValidation(ViewmMdel viewModelInstance)
{
//viewModelInstance.IngestedDataGrid Property
//Check if not sorted
//Display with red border
}
Code Behind based approach
OR i could Add a Code behind file setting the red border of DataGridRow
since i can easily iterate over the ObservableCollection bound to View here as well
OR if you have a better solution in mind please speak up.

How to lazy-evaluate a wpf:DataGrid, retrieving data only as needed

I have a WPF datagrid, bound to a list populated by linq-to-sql from a database. The binding is two-Way, allowing the user to change the values in each row
<wpf:DataGrid AutoGenerateColumns="False"
ItemsSource="{Binding MyList}"
SelectedItem="{Binding SelectedItem}" >
When displaying about 20000 rows, the program crashes with an out-of-memory exception during initialisation of the list. Performance becomes unbearably slow even with less rows.
I know that on initialization the datagrid iterates through each row to measure the maximum column width and other properties. It will apparently do so for all rows regardeless of whether they are on screen.
I tried either binding the datagrid to myQuery.ToList() (to allow sorting the datagrid by clicking on the columns) or binding directly to the IQueryable. (Sorting does not work with that)
Both produce the same result. The ToList() with 20000 items alone does not cause the massive memory consumption, this happens only when it is bound to the datagrid .
Ignoring the issue of how useful 20000 rows in a datagrid are (those are the current requirements; to change those a working example would be helpful).
What is the most simple way to lazily load only the data currently shown on the screen, and ignore everything else until it is scrolled into view?
Can this be done without third party libraries and major code changes?
If not, what would be the recommended workaround?
It turns out that the problem was entirely user error on my part:
WPF Datagrid can do UI virtualisation just fine: The memory consuming row objects are only drawn when required; if a row is outside the visible bounds of the datagrid, it will not be instantiated.
This will however not work if the datagrid is contained inside a ScrollViewer.
Inside a scrollviewer every part of the datagrid is virtually visible, so the entire datagrid will be rendered. The scrollviewer then shows only the part of this rendered datagrid that fits inside the UI window.
Since a datagrid inside a scrollviewer looks just like a datagrid that manages its own scrollbars, I did not notice the scrollviewer.
With the scrollviewer removed, large amounts of rows cause no problem at all, even with variable height and width. The datagrid simply fills the available space, and only instantiates new rows as required.
So in short, the solution for my problem was: Do not put a datagrid inside a scrollviewer
The property that you are binding in this his case let's say that your MyList is made up of MyFile objects (List<MyFile>) then you have to create your MyFile class as:
class MyFile
{
public string FullPath { get; set; }
public string Extension
{
get
{
return Path.GetExtension(FullPath);
}
}
public string PathRoot
{
get
{
return Path.GetPathRoot(FullPath);
}
}
public DateTime CreationTime
{
get
{
return File.GetCreationTime(FullPath);
}
}
}
That way you will store less info in each object and jut call the get method on the few items that are displayed in the grid. Versus having the actual values stored in the class. Hope this helps

Segmentation and displaying content from dictionary

Through my initial program code, I have been able to generate a dictionary of {string, int} fo some some length (mostly 3).
In my application, I have to display the strings separated by a semi-colon, and on hovering over a certain string, it should display the corresponding int of the string as something sort of a tooltip
My idea of doing this was to have the labels (equal to number of strings in dictionary) being displayed, content of each label being picked up from the dictionary's keys, and displaying the tooltip from corresponding value pair.
Though, I am able to generate these labels as said above, I am not sure, how should I be displaying them in a way such Label1 Content ; Label2 Content ; Label3 Content
Is this the correct approach of doing this thing? And how should the labels be displayed according to the above layout (in a stack panel or something? how would these be separated by ; if I have the labels set as the children of stack panel). I am not quite sure.
You will want to use an ItemsControl, with its ItemsPanel set to a StackPanel with Orientation of Horizontal.
Have a look here - Use different template for last item in a WPF itemscontrol.
This shows a solution in XAML that will allow you to omit a semi-colon after the final item in the ItemsControl.
The other thing you may wish to do is convert your dictionary into an ObservableCollection of an 'Item' type or something similar, which means you can use property names in your ItemControl template bindings, which is neater. Something like:
var items = dictionary.Select(kv => new Item { Name = kv.Key, Value = kv.Value });
this.Items = new ObservableCollection<Item>(items);
Where Items is an ObservableCollection<Item> type, and Item is defined as:
public class Item
{
public string Name { get; set; }
public int Value { get; set; }
}
Note that if you instantiate the ObservableCollection in anything other than the constructor, then your Items property setter should invoke the PropertyChanged event, so that the UI is notified of the change.
I don't know how many items you are rendering, but you could also use a WrapPanel, which would allow the Labels to wrap after they reach a certain width. Note that the WrapPanel implemented in WPF doesn't virtualise though, so it will not perform as well for a large number of items.

Categories