Passing header name of GridViewColumn to ViewModel using Caliburn.Micro - c#

I have a GridView inside a ListView in a WPF application. Upon clicking the column header the list items get sorted alphabetically.
XAML:
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}" Width="200">
<GridViewColumn.HeaderContainerStyle>
<Style TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="cal:Message.Attach" Value="[Event Click]=[Action Sort($dataContext, 'Name')]"/>
</Style>
</GridViewColumn.HeaderContainerStyle>
</GridViewColumn>
And here's the sorting method in my ViewModel:
public void Sort(ShellViewModel dataContext, string propertyName)
{
// sorting logic
}
That works perfectly fine. But instead of passing 'Name' (or whatever the header is called), I'd like that to be a property.
I've tried Value="[Event Click]=[Action Sort($dataContext, $eventArgs.PropertyName)]" but that doesn't work and returns null.

You might want to consider changing the listview to a datagrid.
A datagrid has column click sorting built in. No code would be necessary. You'd need to change your markup to a datagrid, which might be non trivial of course. There may be some reason to pick listview instead of datagrid.
If that doesn't suit then the current approach looks like it is going to be tricky.
Looking at the msdn article about sorting a listview. That handles the event.
The piece of code finding the propertyname is:
var headerClicked = e.OriginalSource as GridViewColumnHeader;
.....
var columnBinding = headerClicked.Column.DisplayMemberBinding as Binding;
var sortBy = columnBinding?.Path.Path ?? headerClicked.Column.Header as string;
You might be able to get the binding from $eventArgs. if you use enough dots.
However, I think you would need to get the path out the binding in code.
I personally wouldn't be so keen on a viewmodel doing that sort of manipulation.
I'd suggest a behaviour if you want to encapsulate it or "just" do the sorting entirely in the view. Very much like
https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/how-to-sort-a-gridview-column-when-a-header-is-clicked?view=netframeworkdesktop-4.8
Maybe you don't want the up down arrows and some other aspects.
If the viewmodel needs to know what the user picked you could add an attached property and bind that to the viewmodel. Setcurrentvalue on that property in the view.
Similarly, you could make that twoway. Handle change of the dependency property and set sort if the viewmodel needs to initially set sort dynamically.
Alternatively, extend the attached property concept. Put a sort togglebutton in the headers and bind to an observabledictionary. One entry per column. Handle property changed in the value of the observabledictionary and apply sort.

Related

Passing DataGridRow as Object to a Converter

Once again I have a little problem with WPF, XAML and probably my own stupidity ;)
I have a DataGrid which is bound to the DataContext. The DataContext is an array of Objects from a Class I made myself (something simple like class Employee with Properties like FirstName, LastName, etc... really nothing special.)
I wanted to create a ToolTip for the rows, so I made a RowStyle where I assigned the ToolTip. It was first bound to a Property named 'Status'. The following code worked fine:
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Setter Property="ToolTip">
<Setter.Value>
<TextBlock Text="{Binding Status Converter={StaticResource StatusToolTipConverter}}"/>
</Setter.Value>
</Setter>
</Style>
</DataGrid.RowStyle>
But then I changed my mind. I don't want to pass only the "Status" Property, now I want to pass the whole Object (Remember: one row = one object). Or in other words: I want to Pass the DataContext of the row to my converter. Since the Object that I want to pass is the DataContext itself, there shouldn't be much to change. So I changed it to:
<TextBlock Text="{Binding Converter={StaticResource StatusToolTipConverter}}"/>
Now here is where things start to go wrong. The converter gets 'null' as value. So I deleted the converter and tried it again. The ToolTip was correctly bound to the Object in the Row. I could proof that to myself because the correct Object name was shown in the tooltip. I even overwrote the .ToString() so that the Name of the Employee gets shown as Object name, which it did correctly without the converter.
so TL;DR: Why does the Converter get 'null' as Value, when the Object is bound correctly WITHOUT the converter?
I found a little workaround. I made a property called 'Self' which returns 'this'. The ToolTip is now bound to 'Self'. It works.

How can I access the data object of a DataGridCell in code?

Basically I've bound the datagrid so that it resembles a timetable of subjects - each row represents a semester of subjects, and each cell within that semester represents a subject.
I'm now trying to add drag and drop functionality so that you can drag additional subjects onto the grid, and this will update the underlying datastructure.
I can use some visual tree methods to find the DataGridCell that the user is dragging the new subject to, but I don't know how to access the value (the subject) that the cell is bound to it in order to replace the blank/placeholder value with the new subject. Is there a way to access the underlying value or should I restructure my entire method of creating this program?
To get the data of the DataGridCell, you can use it's DataContext and the Column property. How to do that exactly depends on what your row data is, i.e. what items you put in the ItemsSource collection of the DataGrid. Assuming your items are object[] arrays:
// Assuming this is an array of objects, object[],this gets you the
// row data as you have them in the DataGrid's ItemsSource collection
var rowData = (object[]) DataGrid.SelectedCells[0].Item;
// This gets you the single cell object
var celldata = rowData[DataGrid.SelectedCells[0].Column.DisplayIndex];
If your row data is more complex, you need to write an according method which translates the Column property and the row data item to the specific value on your row data item.
EDIT:
If the cell you drop your data into is not the selected cell, one option is to get the DataGridRow to which the DataGridCell belongs, using VisualTreeHelper:
var parent = VisualTreeHelper.GetParent(gridCell);
while(parent != null && parent.GetType() != typeof(DataGridRow))
{
parent = VisualTreeHelper.GetParent(parent);
}
var dataRow = parent;
Then you have the row and can proceed as above.
Furthermore, regarding your question whether you should reconsider the method, I would suggest using custom WPF behavior s.
Behaviors provide a very straight forward way to extend the control's capabilities from C# code, rather the XAML, yet keeping your codebehind clear and simple (which is not only nice to have if you're following MVVM). Behaviors are designed in way that they are reusable and not bound to your specific control.
Here's a good introduction
For your special case, I can only give you an idea of what to do:
Write one DropBehavior for your the TextBlock control (or whatever control you want inside your DataGridCells, which handles the drop. The basic idea is to register according actions to the evnt of the cells in the OnAttached() method of your control.
public class DropBehavior : Behavior<TextBlock>
{
protected override void OnAttached()
{
AssociatedObject.MouseUp += AssociatedObject_MouseUp;
}
private void AssociatedObject_MouseUp(object sender, MouseButtonEventArgs e)
{
// Handle what happens on mouse up
// Check requirements, has data been dragged, etc.
// Get underlying data, now simply as the DataContext of the AssociatedObject
var cellData = AssociatedObject.DataContext;
}
}
Note that, parsing the data of the single cell from the row data and the Column property becomes obsolete.
Then you attach this behavior to TextBlocks, which you put inside your cells, using the ContentTemplate of the CellStyle of your DataGrid:
<DataGrid>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding}">
<i:Interaction.Behaviors>
<yourns:DropBehavior/>
</i:Interaction.Behaviors>
</TextBlock>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.CellStyle>
</DataGrid>
You can find the Behavior<T> baseclass in
System.Windows.Interactivity.dll
I haven't tested it, but I hope it works for you and you get the idea...

Pass info from Parent View to Child ViewModel

I have in my ParentView(DashboardConsultants) a gridview which shows a custom tooltip when the user's mousepointer is hovered over a cell. The tooltip show a View (AgreementDetails_View) which shows information of the Agreement binded to that cell. I will show the code I have now so you can better understand my question:
DataGrid Cell in ParentView:
<DataGridTextColumn Header="Okt" Width="*" x:Name="test">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Tag" Value="{Binding Months[9].AgreementID}"/>
<Setter Property="Background" Value="White" />
<Setter Property="DataGridCell.ToolTip" >
<Setter.Value>
<v:UC1001_AgreementDetails_View Background="#FFF" Opacity="0.88" />
</Setter.Value>
</Setter>
My ChildView:
public UC1001_DashBoardConsultants_View(UC1001_DashboardConsultantViewModel viewModel)
{
InitializeComponent();
this.DataContext = viewModel;
}
In the ViewModel, I have following method to get the right Agreement from the database:
private void GetRefData()
{
UC1001_ActiveAgreementArguments args = new UC1001_ActiveAgreementArguments();
args.AgreementID = 3;
DefaultCacheProvider defaultCacheProvider = new DefaultCacheProvider();
if (!defaultCacheProvider.IsSet("AgrDet:" + args.AgreementID))
{
ConsultantServiceClient client = new ConsultantServiceClient();
AgreementDetailsContract = client.GetAgreementDetailsByAgreementID(args);
defaultCacheProvider.Set("AgrDet:" + args.AgreementID, AgreementDetailsContract, 5);
}
else
{
AgreementDetailsContract = (UC1001_ActiveAgreementContract)defaultCacheProvider.Get("AgrDet:" + args.AgreementID);
}
}
As you can see for now, the method always calls the same Agreement, (I did that for testing purposes) but now I want the Agreement which ID is specified in the DataGrid Cell Tag (in this example it's the Months[9].AgreementID).
I can give it to the ViewModel in my Child View's constructor, but I don't think that it's allowed due to the MVVM Pattern (or is it allowed?).
So my question is: How can I pass the AgreementID specified in my ParentView to the ChildView's ViewModel to get the right data for the ChildView?
Ofcourse, more information/code/clarification can be happily provided, just ask :)
Thanks in advance!
Not sure that I got question in righ way but my feelings like you need to use Commands instead of introducing tied coupling by passing back reference to parent itself
Personally, I feel that WPF Views should be nothing more than a pretty reflection of the ViewModel. So the View should not actually be passing any data to the ViewModels - instead it should be reflecting the ViewModel's data.
In your case, I would attach a property to the object that is displayed in each DataGrid Row. For example, if your DataGrid contained Agreement objects, I would ensure that each Agreement object had a property called AgreementDetails which can be viewed from the ToolTip
It's pefectly legal and valid to pass in that ID eitehr via a constructor or through a property. I'm not sure from your code, but if your parent is the one with access to your model, you can also pass teh model into your view model (i.e. via constructor, property, or method).
One thing I often do in this case is to add a property such as the following in the ViewModel of my Parent:
object ActiveItem {get;set;}
I then bind that ActiveItem to the ActiveItem in my grid.
<DataGrid SelectedItem="{Binding ActiveItem}">
</DataGrid>

How to set focus to textbox using MVVM?

How to focus a textbox from ViewModel wpf?
<TextBox Name="PropertySearch"
Text="{Binding UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay, Path=PropertySearch,
ValidatesOnDataErrors=True}"
Width="110"
Height="25"
Margin="10" />
You can do this by adding a property to your ViewModel (or use an existing property) that indicates when the SetFocus should happen but the View should be responsible for actually setting the focus since that is purely View related.
You can do this with a DataTrigger.
View:
<Grid Name="LayoutRoot" DataContext="{StaticResource MyViewModelInstance}">
<Grid.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding UserShouldEditValueNow}" Value="True">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=PropertySearch}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<TextBox Name="PropertySearch" Text="{Binding UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Path=PropertySearch, ValidatesOnDataErrors=True}" Width="110" Height="25" Margin="10" />
</Grid>
ViewModel:
// When you think the view should set focus on a control
this.UserShouldEditValueNow = true;
The example above is simplified by just using a boolean ViewModel property "UserShouldEditValueNow". You can add a property like this to your ViewModel or use some other exising property that indicates this state.
Note: So why is it done this way in MVVM? One reason is, suppose the View author decided to replace the TextBox with a ComboBox, or even better, suppose your property was an integer value that had both a TextBox to view/edit the number and a Slider as another way to edit the same value, both controls bound to the same property... how would the ViewModel know which control to set focus on? (when it shouldn't even know what control, or controls, are bound to it in the first place) This way the View can select which control to focus by changing the ElementName binding target in the DataTrigger Setter.
Happy coding!
The question you should be asking yourself is "why does my ViewModel need to know which control has the focus?"
I'd argue for focus being a view-only property; it's an interaction property, and has nothing to do with the conceptual state. This is akin to the background color of a control: why would you represent it in the VM? If you need to manage the focus in a custom way, it's probably better to use a view-level object to do the job.
In your parent control, add the following property:
FocusManager.FocusedElement="{Binding ElementName=PropertySearch}"
While purists may argue for leaving this out of the VM, there are cases where it may make sense to do so from the VM.
My approach has been to make the view implement an interface, pass that interface to the ViewModel, and then let the VM call methods on the interface.
Example:
public interface IFocusContainer
{
void SetFocus(string target);
}
A couple things to keep in mind:
A VM might serve more than one instance of a view, so your VM might want to have a collection of references to IFocusContainer instances, not just one.
Code the VM defensively. You don't know whether there are 0, 1 or 20 views listening.
The "target" parameter of SetFocus() should probably be "loosely" coupled to the VM. You don't want the VM caring about the exact control names in the UI. Rather, the VM should indicate a name that is defined solely for focus management. In my case, I created some attached properties that would allow me to "tag" controls with "focus names".
To implement the interface, you can:
Implement it in the code-behind
Create some behaviors that know how to attach to the ViewModel that is present in the DataContext.
There's nothing wrong with implementing it on the Code Behind, but the behavior approach does allow a XAML only hookup if that's important to you.
In the implementation of the interface, you can use the visual tree to locate the control, or you could just code up a switch statement for a known set of focusable items.

Issue with binding Collection type of dependency property in style

I have a customcontrol exposing a Dependency property of type ObservableCollection. When i bind this properrty directly as part ofthe control's mark up in hte containing control everythihng works fine
<temp:EnhancedTextBox
CollectionProperty="{Binding Path=MyCollection, Mode=TwoWay}"/>
But when i try to do the binding in the style created for the control it fails,
<Style x:Key="abc2"
TargetType="{x:Type temp:EnhancedTextBox}" >
<Setter Property="CollectionProperty"
Value="{Binding Path=MyCollection, Mode=TwoWay}"/>
</Style>
Please help !!!!!
Thanks
It has to do with your data context for the style. There is no way for the style to know where MyCollection is coming from because although you may have it defined in the same file, the style does not share the data context.
I would also ask the question as to why you are setting the property in the style? The style is not meant for this sort of operation. The style is supposed to control the look of the UI element not provide the functionality.

Categories