WPF MVVM Perform different actions based on focused item - c#

I’m developing a WPF application in MVVM Patern. The application has a command bar and buttons for Save and Delete records.
The application also has a Master Detail form. It’s a User control and a DataGrid.
Master block : Customer Order
Detail block: Customer Order Lines
(one to many relationship).
Problem:
When clicking a button in command bar, different actions need to be performed depending on the focused item.
For an example if I click the Delete button
It should delete the records only in the DataGrid row, when DataGrid has
focus and row(s) selected.
E.g. DeleteRows() Method should be called.
It should delete the entire record if the master block has focus and not datagrid focused.
E.g. DeleteRecord() Method should be called.
As far as I know I can achieve this using Keyboard focus and Logical focus manager.
But I was unable to find out a proper solution. I should consider that, when clicking the delete button I should ignore the focus of the Delete button.
Please help me to overcome this issue with a sample code.

Since you're using the MVVM pattern, I assume that your buttons in the command bar have corresponding ICommands in the view model.
You can bind your DataGrid's SelectedItem property to a view model property (of course, with a two-way binding) and make that decision according to this property value. If it is null, so there's no item currently selected in the DataGrid, and you can delete the whole record. If it is set to an instance, then a row is selected in the DataGrid, and you can delete only one row.
If you need to exactly know which was the last focused element, you can use the Keyboard.PreviewLostKeyboardFocus attached event in your code behind. Or even better, create your own Behavior with a dependency propery that you can bind to your view model.
enum LastFocusedEntityType { None, Record, Row }
class LastFocusedEntityTrackingBehavior : Behavior<UIElement>
{
public static readonly LastFocusedEntityProperty = DependencyProperty.Register(
"LastFocusedEntity",
typeof(LastFocusedEntityType),
typeof(LastFocusedEntityTrackingBehavior),
LastFocusedEntityType.None);
public LastFocusedEntityType LastFocusedEntity
{
get { return (LastFocusedEntityType)this.GetValue(LastFocusedEntityProperty); }
set { this.Setvalue(LastFocusedEntityProperty, value); }
}
protected override void OnAttached()
{
Keyboard.AddPreviewLostKeyboardFocusHandler(this.AssociatedObject, this.PreviewLostKeyboardFocusHandler);
}
private void PreviewLostKeyboardFocusHandler(object sender, KeyboardFocusChangedEventArgs e)
{
if (e.OldFocus is DataGrid)
{
this.LastFocusedEntity = LastFocusedEntityType.Row;
}
else
{
this.LastFocusedEntity = LastFocusedEntityType.Record;
}
}
}
Then you can apply this behavior to your master block container:
<UserControl>
<i:Interaction.Behaviors>
<local:LastFocusedEntityTrackingBehavior LastFocusedEntity="{Binding LastFocusedEntity, Mode=OneWayToSource}"/>
</i:Interaction.Behaviors>
</UserControl>
In your view model, your ICommand's Execute() method should then look at the LastFocusedEntity property value and decide what to do next.
Note: I didn't check this code whether it compiles.

Related

AddingItemEvent not adding the item to the ItemSource c#

I created a pretty straight forward desktop application with WPF and net6.0. The application contains a DataGrid with the following XAML code:
<DataGrid x:Name="tblTopics"
CanUserAddRows="True"
AddingNewItem="tblTopics_AddingNewItem"
/>
The binding was done during the initalization of the application with the following statement:
tblTopics.ItemsSource = Topics;
Where Topics is an ObservableList containing the class Topic. Everything works as intended and all rows are properly loaded in the application. Also, if I edit some rows, the changes are correctly transfered to the ItemSource.
However, if a new item is added via clicking in the empty row at the bottom, this change or rather addition is not reflected in the ItemSource. My code for AddingNewItem looks as follows:
private void tblTopics_AddingNewItem(object sender, AddingNewItemEventArgs e)
{
Debug.WriteLine("New Topic added...");
Topic newTopic = new Topic(TopicType.NO_TYPE, "Title", "Description", DateTime.Now, "Responsible");
e.NewItem = newTopic;
}
Interestingly, if I add the item "manually" via code upon a click on a button to the ItemSource, the item will be displayed in the application as well. No problem there. But what am I doing wrong with the above approach? Why is the newly added item not being transferred to the ItemSource?
By accident I found the solution to my problem, thx to WPF DataGrid - Event for New Rows?
The issue is that "Objects are persisted (inserted or updated) when the user leaves a row that he was editing. Moving to another cell in the same row updates the corresponding property through data binding, but doesn't signal the Model (or the Data Access Layer) yet. The only useful event is DataGrid.RowEditEnding. This is fired just before committing the modified row."
So accordingly, upon the RowEditEnding event, I check whether the user committed a change and if so, I add the rowItem to the itemsource.
private void tblTopics_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
{
if (e.EditAction == DataGridEditAction.Commit)
{
var newItem = e.Row.DataContext as ItemSourceDataType;
if (!MyItemSource.Contains(newItem)){
//add newItem to itemsource
}
}
}
}

Modifications to the default behavior in Syncfusion Blazor Grid

I have a grid that is working for me, but my Customer has a requirement pretty different to the default behavior. I'm now very pressed to perform this changes ASAP.
The grid has a checkbox column and multiselection is enabled. The user can select a row only through the checkboxes. For the sake of brevity, I'll copy here the settings.
<SfGrid #ref="Grid" DataSource="Coberturas" Width="100%" EnablePersistence="true">
<GridSelectionSettings Type="SelectionType.Multiple" CheckboxMode="CheckboxSelectionType.Default" CheckboxOnly="true"></GridSelectionSettings>
<GridEditSettings AllowAdding="false" AllowDeleting="false" AllowEditing="true" AllowEditOnDblClick="false" AllowNextRowEdit="false"></GridEditSettings>
It has no toolbar. You can't add rows, nor deleting them. EnablePersistence is needed for reasons out of this question. There is a Primary Key column, but it is hidden (Visible is false). About the rest of the columns, they are text columns.
The customer has asked to change the default behavior in the following way:
When the user clicks a checkbox, the row must both select and open
for editing.
After changing values, the edition boxes must remain
open, even if the user press Enter or clicks another row.
If the user clicks another row, this new row must both select and open for
editing. Even if it's not possible to kept the previous row with the
boxes open (I'm in doubt about if this is possible in the Syncfusion
grid), the previous selection must be kept. All selections must not
be lost, even if the user confirms the edition or moves to another
row.
Illustrative image of the requirements
I'm pretty new to Syncfusion controls and I have no idea how to perform this requirement. I suppose I must create handlers for some Grid Events (maybe RowSelected or OnRecordClick?) but I also may have to interrupt the default Selection and Editing behavior and I don't know how to do this. Because the pressure I have, any idea would be highly appreciated.
You can achieve this requirement by using the RowSelected and OnActionComplete events of Grid. Call the StartEdit method in RowSelected event handler to enable edit in a single click. And in the OnActionComplete event handler based on the RequestType as BeginEdit you can select the required rows based on the stored selected rows indexes from RowSelected event handler.
Sample : https://www.syncfusion.com/downloads/support/directtrac/general/ze/ServerApp-614805550
Please refer the codes below,
<GridEvents RowSelected="RowSelected" OnActionComplete="OnActionComplete" TValue="Order"></GridEvents>
public List<double> SelectIndexes = new List<double>();
SfGrid<Order> Grid;
public bool flag { get; set; } = true;
public async Task RowSelected(RowSelectEventArgs<Order> args)
{
if (flag)
{
await Grid.StartEdit();
SelectIndexes.Add(args.RowIndex);
}
flag = true;
}
public async Task OnActionComplete(ActionEventArgs<Order> args)
{
if (args.RequestType.Equals(Action.BeginEdit) && SelectIndexes.Count != 0)
{
flag = false;
await Grid.SelectRows(SelectIndexes);
}
}
References :
https://blazor.syncfusion.com/documentation/datagrid/events/#onactioncomplete
https://help.syncfusion.com/cr/blazor/Syncfusion.Blazor.Grids.SfGrid-1.html#Syncfusion_Blazor_Grids_SfGrid_1_StartEdit
https://blazor.syncfusion.com/documentation/datagrid/editing/#event-trace-while-editing

What could be preventing my combobox dropdown from showing after app focus is lost?

I have a ComboBox in a WPF app that has recently been refactored to use the MVVM pattern. An apparent side effect to this change is that changing focus to another application while the combobox dropdown is visible completely prevents the dropdown from being visible again, until the app has been restarted.
The ComboBox DataContext is set to my ViewModel, with its ItemsSource bound to an ObservableCollection<String> SearchSuggestions, and IsDropdownOpen bound to a property SuggestionsVisible in the ViewModel.
The desired effect is a search box with autocomplete suggestions. It should close if there are no suggestions in the ObservableCollection, if the user cancels the search, if the user runs the search, or if the user clicks away from the text field - either inside the app or outside it.
The ViewModel explicitly sets the SuggestionsVisible property to true or false based on whether SearchSuggesions contains any items after user input. This process continues to take place after this bug manifests itself, just with no visible change to the UI. Any idea why losing focus while the dropdown is open renders the dropdown un-openable for the rest of the app's session?
Here's how I have things wired together:
<ComboBox DataContext="{Binding SearchBoxVm}" Name="cmboSearchField" Height="0.667"
VerticalAlignment="Top" IsEditable="True" StaysOpenOnEdit="True"
PreviewKeyUp="cmboSearchField_OnKeyUp"
PreviewMouseLeftButtonUp="cmboSearchField_OnPreviewMouseLeftButtonUp"
Background="White" ItemsSource="{Binding SearchTopics}"
IsDropDownOpen="{Binding SuggestionsVisible,
UpdateSourceTrigger=PropertyChanged}"
Margin="50.997,15.333,120.44,0"
RenderTransformOrigin="0.5,0.5" Grid.Row="1" >
<!-- SNIP STYLING -->
</ComboBox>
ViewModel:
public class SearchBoxViewModel : INotifyPropertyChanged
{
public void ResetSearchField(bool preserveContents = false)
{
if (!preserveContents || string.IsNullOrEmpty(Query))
{
Foreground = Brushes.Gray;
QueryFont = FontStyles.Italic;
Query = DEFAULT_TEXT;
}
}
public bool OnKeyUp(Key key)
{
bool showDropdown = SuggestionsVisible;
bool changeFocusToCombobox = false;
if (keyInValidRange(key))
{
SearchSuggestions = GetSearchSuggestions(Query);
if (SearchSuggestions.Count > 0)
{
SuggestionsVisible = true;
}
}
return changeFocusToCombobox;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
bool _suggestionsVisible = false;
public bool SuggestionsVisible
{
get { return _suggestionsVisible; }
set
{
// this section is still called after this issue manifests,
// but no visible change to the UI state is made
_suggestionsVisible = value;
NotifyPropertyChanged("SuggestionsVisible");
}
}
public ObservableCollection<String> SearchTopics = new ObservableCollection<String>();
}
The OnKeyUp() method is called by the MainWindow class ( haven't gotten as far as binding events to handlers specified in the ViewModel ), while but there's also a call to ResetSearechField from the MainWindow:
// Note: removing references to this event handler does not have any effect
// on the issue at hand... only including here for completeness
void window_Deactivated(object sender, EventArgs e)
{
SearchBoxVm.SuggestionsVisible = false;
SearchBoxVm.ResetSearchField(true);
}
I've spent quite a bit of time trying to debug this, and haven't seen any internal state changes that might account for this. The NotifyPropertyChanged event is otherwise behaving as it did before, and the stack trace window isn't showing any exceptions having been encountered.
Setting the binding mode on the IsDropdownOpen property to 'TwoWay' in the XAML hasn't had any effect either. Lastly, wrapping the assignment to SuggestionsVisible in a Dispatcher call on the main thread has had no effect on the issue either.
Any assistance would be appreciated.
#BrMcMullin, since you have stated that:
The desired effect is a search box with autocomplete suggestions.
may I ask, why do you choose to use standard ComboBox instead of specialized AutoCompleteBox that is available in the WPF Toolkit - February 2010 Release and seems like was especially designed for your case?
You may have noticed that first link points to documentation for its Silverlight predecessor, but don't worry - WPF Toolkit library include fully functional official WPF port of AutoCompleteBox from Silverlight. There is more info about this "event": AutoCompleteBox: Now with 100% more WPF.
With that control your auto complete popup could looks as simple as:
or as complex as:
So, if you will not manage to solve your issue with ComboBox's popup visibility, feel free to give a try to AutoCompleteBox. With it you could even leverage dynamic sorting of your suggestions if needed (just use answer from #adabyron).

When implementing ICommand in MVVM I'm missing something

I'm very new to C# and WPF in general, having come from a LAMP web application background. I've tried following a couple tutorials on the subject but they have left me stumped.
https://rachel53461.wordpress.com/2011/05/08/simplemvvmexample/
http://www.codeproject.com/Articles/165368/WPF-MVVM-Quick-Start-Tutorial
In the first tutorial the application opens with a simple interface. A TextBox and two buttons. When you update the ProductId TextBox, going from 0 to 1, the "Get Product" button becomes active. When you click on the "Get Product" button the DataTemplate's contents appear. I can't see where these things are actually happening. What causes the button to become active? What causes the form to appear?
Hopefully you guys and gals can dumb it down for me =)
The buttons become activated or deactivated since they're bound to an ICommand. ICommand includes a CanExecute property, which determines whether the button is active or not.
I can't see where these things are actually happening.
The command uses delegates:
_getProductCommand = new RelayCommand(
param => GetProduct(),
param => ProductId > 0
);
The second delegate causes the command to be activated (CanExecute becomes true) when ProductId > 0 (the delegate returns true).
When you click the button, the command's Execute method fires, and performs the actions.
As for the window starting up in the first place, look at the section titled "Starting the Sample" - there's code in app.xaml.cs which is used to display the Window at first.
In that specific example you can see:
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding ProductId}" />
The TextBox uses data binding to bind its Text property to the ProductId on the current data context (which is an instance of the view model).
When you type text in the TextBox, its value will be automatically updated on the ProductId property on the view model.
The command checks the value of the ProductId is greater than 0 to determine if the Button is enabled.
You can see the Button's command is set to the GetProductCommand:
<Button ... Command="{Binding Path=GetProductCommand}" ... />
The GetProductCommand uses the expression ProductId > 0 as its CanExecute predicate:
_getProductCommand = new RelayCommand(
param => GetProduct(),
param => ProductId > 0
);
WPF will execute this lambda expression ProductId > 0 whose result will determine if the Button is enabled. When the button is enabled and clicked, the first lambda expression will be executed - GetProduct().
Having said all this, you really should be using an MVVM framework, and these frameworks have other mechanisms for invoking methods on your view model which get past some of the limitations of commanding.
Essentially the Idea behind MVVM or Model, View, ViewModel is to remove that code behind and separate out the layers of the application so they can be worked on independently. The interaction between your GUI or View and Viewmodel or codebehind isn't happening like you probably think it is. A lot of people get confused on how the viewmodel actually interacts with the gui or view. I was a winforms dev with the code behind file in which you could easily see event handlers in the code behind and follow the code logically. In your MainWindow Code behind your setting the data context of the XAML gui to the view model like so.
public partial class MainWindow : Window
{
#region Members
SongViewModel _viewModel;
int _count = 0;
#endregion
public MainWindow()
{
InitializeComponent();
// We have declared the view model instance declaratively in the xaml.
// Get the reference to it here, so we can use it in the button click event.
_viewModel = (SongViewModel)base.DataContext;
}
private void ButtonUpdateArtist_Click(object sender, RoutedEventArgs e)
{
++_count;
_viewModel.ArtistName = string.Format("Elvis ({0})", _count);
}
}
Then the {Binding Path=Property} is wiring up your properties of your _viewModel to the XAML Elements. Adding the RaisePropertyChanged is what is notifying the gui that the value of that property has changed.
public string ArtistName
{
get { return Song.ArtistName; }
set
{
if (Song.ArtistName != value)
{
Song.ArtistName = value;
RaisePropertyChanged("ArtistName");
}
}
}
The ArtistName Property of the View model is bound to the label like so in XAML
This is your communication between gui and code more or less. So for instance in your first example when you changed the textbox from 0 to 1, that then updates your ProductID property in your ViewModel. You can see in your ICommand GetProductCommand there is a parameter being passed to relayCommand for ProductID > 0. Now that its 1 canexecute is true so the command is now available to execute and the button becomes clickable. When you Click it GetProduct is the action that takes place and that method then sets your CurrentProduct Property with value. The Datatemplate for product information is bound to ProductModel in the xaml and ProductModel is bound to your CurrentProduct, so now inside that datatemplate the properties of CurrentProduct can be bound to xaml elements so CurrentProduct.ProductName or is bound to
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ProductName}" />
Its confusing at first but when you get the hang of it, it will make total sense.

How to pass information from one WPF UserControl to another WPF UserControl?

I've got a WPF application.
On the left side there is a stackpanel full of buttons.
On the right side there is an empty dockpanel.
When user clicks a button, it loads the corresponding UserControl (View) into the dockpanel:
private void btnGeneralClick(object sender, System.Windows.RoutedEventArgs e)
{
PanelMainContent.Children.Clear();
Button button = (Button)e.OriginalSource;
Type type = this.GetType();
Assembly assembly = type.Assembly;
IBaseView userControl = UserControls[button.Tag.ToString()] as IBaseView;
userControl.SetDataContext();
PanelMainContent.Children.Add(userControl as UserControl);
}
This pattern works well since each UserControl is a View which has a ViewModel class which feeds it information which it gets from the Model, so the user can click from page to page and each page can carry out isolated functionality, such as editing all customers, saving to the database, etc.
Problem:
However, now, on one of these pages I want to have a ListBox with a list of Customers in it, and each customer has an "edit" button, and when that edit button is clicked, I want to fill the DockPanel with the EditSingleCustomer UserControl and pass it the Customer that it needs to edit.
I can load the EditCustomer usercontrol, but how do I pass it the customer to edit and set up its DataContext to edit that customer?
I can't pass it in the constructor since all the UserControls are already created and exist in a Dictionary in the MainWindow.xaml.cs.
so I created a PrepareUserControl method on each UserControl and pass the Customer to it and can display it with a textbox from code behind with x:Name="..." but that is not the point, I need to DataBind an ItemsControl to a ObservableCollection to take advantage of WPF's databinding functionality of course.
so I tried to bind the ListBox ItemSource in the View to its code behind like this:
<UserControl.Resources>
<local:ManageSingleCustomer x:Key="CustomersDataProvider"/>
</UserControl.Resources>
<DockPanel>
<ListBox ItemsSource="{Binding Path=CurrentCustomersBeingEdited, Source={StaticResource CustomersDataProvider}}"
ItemTemplate="{DynamicResource allCustomersDataTemplate}"
Style="{DynamicResource allCustomersListBox}">
</ListBox>
</DockPanel>
which gets a stackoverflow error caused by an endless loop in the IntializeComponent() in that view. So I'm thinking I'm going about this in the wrong way, there must be some easier paradigm to simply pass commands from one UserControl to another UserControl in WPF (and before someone says "use WPF commanding", I already am using commanding on my UserControl that allows the user to edit all customers, which works fine, but I have to handle it in my code behind of my view (instead of in my viewmodel) since I need the parent window context to be able to load another user control when its finished saving:
<Button Style="{StaticResource formButton}"
Content="Save"
Command="local:Commands.SaveCustomer"
CommandParameter="{Binding}"/>
private void OnSave(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
Customer customer = e.Parameter as Customer;
Customer.Save(customer);
MainWindow parentShell = Window.GetWindow(this) as MainWindow;
Button btnCustomers = parentShell.FindName("btnCustomers") as Button;
btnCustomers.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
}
So how in WPF can I simply have a UserControl loaded in a DockPanel, inside that UserControl a button with a command on it that loads another UserControl and sends that UserControl a specific object to which it can bind its controls?
I can imagine I just do not know enough about WPF commands at this point, if anyone can point me in the right direction from here, that would be great, or if you think this "loading UserControls in a DockPanel pattern is foreign to WPF and should be avoided and replaced with another way to structure applications", that would be helpful news as well. You can download the current state of my application here to get an idea of how it is structured. Thanks.
I've just finished a LOB application using WPF where this sort of problem/pattern appeared constantly, so here's how I would have solved your problem:
1) In the DataTemplate where you create each item in the ListBox, along with it's edit button, bind the Button's tag property to the Customer object underlying that list box item.
2) Create a Click event handler for the button, and set the Button's Click event to fire the handler.
3) In the event handler, set the Content property of the UserControl.
4) Set up a DataTemplate in scope of the User Control (perhaps in the resources of it's immediate container) which describes an editor for that single customer.
Another approach that will work is to declare a Customer dependency property on your EditCustomer class, then set that property (perhaps through a XAML Trigger) when the button is clicked.
I hope this isn't too vague. If nothing else, know that the problem you're facing is very solvable in WPF.
This is where you use the Mediator pattern. There's several blog posts on this topic (for instance), and there's implementations of the pattern in some WPF frameworks (such as EventAggregator in Prism).
I don't have the time to really dig into this (it's an interesting question and I hope you get a good answer-- I can see myself running into a similar situation in the future).
Have you considered getting a little less WPF-y and falling back to firing an event on your source UserControl with an EventArgs that contains the customer, then in the event handler, firing the appropriate command on the target control?

Categories

Resources