How to implement Text Search in CefSharp - c#

I'm building an application using CefSharp and need to provide text search functionality to user just like Google Chrome has.
Can any one help me with the implementation of text search in CefSharp?

You can do it simple just add two buttons and a textbox on your form.
first buttons for next result,second buttons for previous result and textbox for search text provider.
On textbox's KeyUp event run below code
if (tosBrowserSearchTxt.Text.Length <= 0)
{
//this will clear all search result
webBrowserChromium.StopFinding(true);
}
else
{
webBrowserChromium.Find(0, tosBrowserSearchTxt.Text, true, false,false);
}
On next button click run below code
webBrowserChromium.Find(0, tosBrowserSearchTxt.Text, true, false, false);
On previous button click run below code
webBrowserChromium.Find(0, tosBrowserSearchTxt.Text, false, false, false);
When user type any character into text box KeyUp event's code will search that text, by using next and previous button you can navigate from one result to another.

I've built this demo application using CefSharp 47.0.3, hopefully this is what you're looking for.
The view:
<Window x:Class="CefSharpSearchDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wpf="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:cefSharpSearchDemo="clr-namespace:CefSharpSearchDemo"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
d:DataContext="{d:DesignInstance {x:Type cefSharpSearchDemo:MainWindowViewModel}}">
<DockPanel>
<DockPanel DockPanel.Dock="Top">
<Button Content="Next" DockPanel.Dock="Right" Command="{Binding ElementName=SearchBehavior, Path=NextCommand}" />
<Button Content="Previous" DockPanel.Dock="Right" Command="{Binding ElementName=SearchBehavior, Path=PreviousCommand}" />
<TextBox DockPanel.Dock="Right" Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"></TextBox>
</DockPanel>
<wpf:ChromiumWebBrowser x:Name="wb" DockPanel.Dock="Bottom"
Address="http://stackoverflow.com">
<i:Interaction.Behaviors>
<cefSharpSearchDemo:ChromiumWebBrowserSearchBehavior x:Name="SearchBehavior" SearchText="{Binding SearchText}" />
</i:Interaction.Behaviors>
</wpf:ChromiumWebBrowser>
</DockPanel>
</Window>
The code-behind for the view:
namespace CefSharpSearchDemo
{
using System.Windows;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
}
The view model:
namespace CefSharpSearchDemo
{
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class MainWindowViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _searchText;
public string SearchText
{
get { return _searchText; }
set
{
_searchText = value;
NotifyPropertyChanged();
}
}
protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And now the important part. As you could see in the view there is a behavior attached to the ChromiumWebBrowser:
namespace CefSharpSearchDemo
{
using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;
using CefSharp;
using CefSharp.Wpf;
public class ChromiumWebBrowserSearchBehavior : Behavior<ChromiumWebBrowser>
{
private bool _isSearchEnabled;
public ChromiumWebBrowserSearchBehavior()
{
NextCommand = new DelegateCommand(OnNext);
PreviousCommand = new DelegateCommand(OnPrevious);
}
private void OnNext()
{
AssociatedObject.Find(identifier: 1, searchText: SearchText, forward: true, matchCase: false, findNext: true);
}
private void OnPrevious()
{
AssociatedObject.Find(identifier: 1, searchText: SearchText, forward: false, matchCase: false, findNext: true);
}
protected override void OnAttached()
{
AssociatedObject.FrameLoadEnd += ChromiumWebBrowserOnFrameLoadEnd;
}
private void ChromiumWebBrowserOnFrameLoadEnd(object sender, FrameLoadEndEventArgs frameLoadEndEventArgs)
{
_isSearchEnabled = frameLoadEndEventArgs.Frame.IsMain;
Dispatcher.Invoke(() =>
{
if (_isSearchEnabled && !string.IsNullOrEmpty(SearchText))
{
AssociatedObject.Find(1, SearchText, true, false, false);
}
});
}
public static readonly DependencyProperty SearchTextProperty = DependencyProperty.Register(
"SearchText", typeof(string), typeof(ChromiumWebBrowserSearchBehavior), new PropertyMetadata(default(string), OnSearchTextChanged));
public string SearchText
{
get { return (string)GetValue(SearchTextProperty); }
set { SetValue(SearchTextProperty, value); }
}
public static readonly DependencyProperty NextCommandProperty = DependencyProperty.Register(
"NextCommand", typeof (ICommand), typeof (ChromiumWebBrowserSearchBehavior), new PropertyMetadata(default(ICommand)));
public ICommand NextCommand
{
get { return (ICommand) GetValue(NextCommandProperty); }
set { SetValue(NextCommandProperty, value); }
}
public static readonly DependencyProperty PreviousCommandProperty = DependencyProperty.Register(
"PreviousCommand", typeof (ICommand), typeof (ChromiumWebBrowserSearchBehavior), new PropertyMetadata(default(ICommand)));
public ICommand PreviousCommand
{
get { return (ICommand) GetValue(PreviousCommandProperty); }
set { SetValue(PreviousCommandProperty, value); }
}
private static void OnSearchTextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var behavior = dependencyObject as ChromiumWebBrowserSearchBehavior;
if (behavior != null && behavior._isSearchEnabled)
{
var newSearchText = dependencyPropertyChangedEventArgs.NewValue as string;
if (string.IsNullOrEmpty(newSearchText))
{
behavior.AssociatedObject.StopFinding(true);
}
else
{
behavior.AssociatedObject.Find(1, newSearchText, true, false, false);
}
}
}
protected override void OnDetaching()
{
AssociatedObject.FrameLoadEnd -= ChromiumWebBrowserOnFrameLoadEnd;
}
}
}
And the minor additional code for the DelegateCommand:
namespace CefSharpSearchDemo
{
using System;
using System.Windows.Input;
public class DelegateCommand : ICommand
{
private readonly Action _action;
public DelegateCommand(Action action)
{
_action = action;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_action();
}
public event EventHandler CanExecuteChanged;
}
}
The resulting application has a TextBox on the top and two buttons labeled "Previous" and "Next" next to it.
The main area is a CefSharp browser which loads http://www.stackoverflow.com.
You can type into the TextBox and it will search in the browser (and highlight the scrollbar where the hits are, just like in Chrome). You can then press the Next/Previous buttons to cycle through the hits.
I hope this helps in developing your own solution.
All this said, let me just note that next time if you ask a question, actually provide some code what you tried, or try to ask a more specific question, because this is probably too broad for this site. Anyway I leave this here, maybe others will find it useful as well.
Important lesson: there are some methods exposed on ChromiumWebBrowser that you can use to implement the search functionality (namely: Find and StopFinding).

Related

MVVM, Can you bind command in ViewModel to Button in ItemsRepeater when ItemsSource is ObservableCollection<Model> and a property of the ViewModel?

I am building a WinUI 3 application that presents a user with a list of Animal IDs, along with a Button beside each ID that the user can click to view information about that specific Animal. I am using an ItemsRepeater, whose ItemsSource is bound to an ObservableCollection<Animal> inside an AnimalViewModel, so I can create a list of TextBox with a Button beside it using a DataTemplate -- the TextBox will have as its value the Animal.ID. I also want to use the MVVM design pattern to accomplish this, but this is my first time trying it.
For now, I want the ability to click a Button and have it display a dialog box with the Animal.ID it corresponds to (the TextBox adjacent to it). I have tried implementing this by using an ICommand and defining a function to display a MessageDialog in my AnimalViewModel, then defining the custom ICommand in its own file.
The issue seems to be that since I have bound my ItemsRepeater to ObservableCollection<Animal> in my AnimalViewModel, I can't bind the Button to the command in my AnimalViewModel. This leads me to believe I am not using MVVM correctly or I have improperly structured my code, but I am not sure what to change/how to move forward.
Error
BindingExpression path error: 'DisplayIDsCommand' property not found on 'MAIT.Models.Animal'
Below is the relevant code. I am able to create the list but I cannot get the Button to exhibit the desired behavior (please forgive any glaring errors, I had to simplify the code for this question):
MainWindow.xaml
<Window
x:Class="MAIT.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MAIT"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:l="using:MAIT.Models"
mc:Ignorable="d">
<Grid x:Name="MainGrid">
<Grid.Resources>
<muxc:StackLayout x:Name="VerticalStackLayout" Orientation="Vertical" Spacing="8"/>
<DataTemplate x:Key="AnimalTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="text" Text="{Binding ID}"></TextBlock>
<Button Command="{Binding DisplayIDsCommand}" CommandParameter="{Binding ElementName=text, Path=Text}">View</Button>-
</StackPanel>
</DataTemplate>
</Grid.Resources>
<ScrollViewer HorizontalScrollBarVisibility="Auto" HorizontalAlignment="Center" VerticalAlignment="Center"
HorizontalScrollMode="Auto"
IsVerticalScrollChainingEnabled="False"
MaxHeight="500">
<muxc:ItemsRepeater
ItemsSource="{Binding Path=Animals}"
Layout="{StaticResource VerticalStackLayout}"
ItemTemplate="{StaticResource AnimalTemplate}"/>
</ScrollViewer>
</Grid>
</Window>
MainWindow.xaml.cs
using Microsoft.UI.Xaml;
using MAIT.ViewModels;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace MAIT
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
MainGrid.DataContext = new AnimalViewModel();
}
}
}
Animal.cs
using System.ComponentModel;
using System.Diagnostics;
namespace MAIT.Models
{
internal class Animal : INotifyPropertyChanged
{
string _ID;
public Animal(string id)
{
ID = id;
}
public string ID
{
get
{
return _ID;
}
set
{
_ID = value;
OnPropertyChanged("ID");
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
AnimalViewModel.cs
using MAIT.Commands;
using MAIT.Models;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data;
using System.Data.OleDb;
using System.Diagnostics;
using Windows.UI.Popups;
namespace MAIT.ViewModels
{
internal class AnimalViewModel : INotifyPropertyChanged
{
public IDsCommand DisplayIDsCommand;
public AnimalViewModel()
{
Animals = new ObservableCollection<Animal>();
DisplayIDsCommand = new IDsCommand(DisplayIDs);
GetAnimals();
}
private void GetAnimals()
{
for(int i = 0; i < 3; i++)
{
Animal animal = new Animal(i.ToString());
Animals.Add(animal);
}
}
public async void DisplayIDs(string id)
{
MessageDialog t = new MessageDialog(id);
await t.ShowAsync();
}
private ObservableCollection<Animal> _Animals;
public ObservableCollection<Animal> Animals
{
get
{
return _Animals;
}
set
{
_Animals = value;
OnPropertyChanged("Animals");
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
IDsCommand.cs
using MAIT.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace MAIT.Commands
{
internal class IDsCommand : ICommand
{
public event EventHandler CanExecuteChanged;
private Action<string> _Execute;
public IDsCommand(Action<string> execute)
{
_Execute = execute;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_Execute.Invoke(parameter as string);
}
}
}
your first WPF appears to be well-on-its-way.
If you want to bind to your command, you will need to use a private field along with the getter and setter, such as with the Animals property, and call OnPropertyChanged in its setter as well. This will let the button know when a command is assigned!
Here are a few friendly pointers if you are interested about learning MVVM:
Consider replacing the using in the local parameter to xmlns:local="clr-namespace:MAIT".
In MVVM DataContext property can be set in the Window's constructor but usually it's put in the XALM above <Grid> as
<Window.DataContext>
<viewModels:AnimalViewModel/>
</Window.DataContext>
Because this context will not allow you to use local.ViewModels, you can declare another xmlns as xmlns:viewModels="clr-namespace:MAIT.ViewModels"
If you are using recent .NET versions, you might appreciate using PropertyChanged?.Invoke(this, new(propertyName)); as the method body for OnPropertyChanged. Moreover, you can use System.Runtime.CompilerServices to automatically get the calling member's name by changing the OnPropertyChanged's signature to private void OnPropertyChanged([CallerMemberName] string propertyName = "").
To be able to bind the Command property to a source property of a parent element, you need to get a reference to the parent element somehow. In Win UI, you cannot use something RelativeSource AncestorType=ItemsRepeater.
You could workaround this by creating an attached property that sets the DataContext of the Button to the parent ItemsRepeater:
public static class AncestorSource
{
public static readonly DependencyProperty AncestorTypeProperty =
DependencyProperty.RegisterAttached(
"AncestorType",
typeof(Type),
typeof(AncestorSource),
new PropertyMetadata(default(Type), OnAncestorTypeChanged)
);
public static void SetAncestorType(FrameworkElement element, Type value) =>
element.SetValue(AncestorTypeProperty, value);
public static Type GetAncestorType(FrameworkElement element) =>
(Type)element.GetValue(AncestorTypeProperty);
private static void OnAncestorTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FrameworkElement target = (FrameworkElement)d;
if (target.IsLoaded)
SetDataContext(target);
else
target.Loaded += OnTargetLoaded;
}
private static void OnTargetLoaded(object sender, RoutedEventArgs e)
{
FrameworkElement target = (FrameworkElement)sender;
target.Loaded -= OnTargetLoaded;
SetDataContext(target);
}
private static void SetDataContext(FrameworkElement target)
{
Type ancestorType = GetAncestorType(target);
if (ancestorType != null)
target.DataContext = FindParent(target, ancestorType);
}
private static object FindParent(DependencyObject dependencyObject, Type ancestorType)
{
DependencyObject parent = VisualTreeHelper.GetParent(dependencyObject);
if (parent == null)
return null;
if (ancestorType.IsAssignableFrom(parent.GetType()))
return parent;
return FindParent(parent, ancestorType);
}
}
Usage:
<DataTemplate x:Key="AnimalTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="text" Text="{Binding ID}"></TextBlock>
<Button local:AncestorSource.AncestorType="muxc:ItemsRepeater"
Command="{Binding DataContext.DisplayIDsCommand}"
CommandParameter="{Binding ElementName=text, Path=Text}">View</Button>-
</StackPanel>
</DataTemplate>
Please refer to this blog post for more information.

C# WPF Update two user controls using dependency property located in another project

I am trying to set a common dependency property to two different user controls in WPF.
I tried lots of solutions that I found but none of them worked.
So for the moment what I got is the following:
I have a class containing the common property which currently (after trying almost everything) looks this way:
namespace CommonProperties {
public class CommonProp : DependencyObject, INotifyPropertyChanged
{
public static readonly DependencyProperty IsTrueProperty =
DependencyProperty.Register("IsTrue", typeof(bool), typeof(CommonProp), new PropertyMetadata(false));
private bool _isTrue;
public bool IsTrue
{
get { return (bool)GetValue(IsTrueProperty); }
set { SetValue(IsTrueProperty, value); NotifyPropertyChanged("IsTrue"); }
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string nomPropriete)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
}
}}
I also have two user controls which looks like that: UC1:
<UserControl x:Class="ClassLibUC1.UC1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:CommonProperties;assembly=CommonProperties"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<Grid>
<TextBox x:Name="text1"
Width="254"
Height="23"
Margin="24,24,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Text="{Binding IsTrue}"
TextWrapping="Wrap" />
<Button Width="75"
Margin="70,68,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Click="Button_Click"
Content="Button" />
</Grid>
UC1 ViewModel:
namespace ClassLibUC1 {
public class ViewModelUC1 : INotifyPropertyChanged
{
CommonProp prop = new CommonProp();
public bool IsTrue
{
get { return prop.IsTrue; }
set { prop.IsTrue = value; NotifyPropertyChanged("IsTrue"); }
}
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Trigger the PropertyChanged event to update views.
/// </summary>
/// <param name="nomPropriete"></param>
public void NotifyPropertyChanged(string nomPropriete)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
}
}
UC1 Code:
public partial class UC1 : UserControl
{
ViewModelUC1 vm = new ViewModelUC1();
public UC1()
{
InitializeComponent();
this.DataContext = vm;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
CommonProp prop = new CommonProp();
if (vm.IsTrue)
{
vm.IsTrue = false;
}
else
{
vm.IsTrue = true;
}
}
}
The second user control is exactly the same. The problem is that when I click a button in the first or the second user control, it only updates the selected control and not both of them.. Any idea how can I implement a common property for both controls?
With Dependency property we cannot achieve this because Dependency property belongs to one instance, means if you have two instance of user control, updating DependencyProperty value of one control will never update DependencyProperty of other Control as it is a different instance all together.
But still we can achieve that requirement of updating Same value for all usercontrols of same type like yours. For that we have to do some modification in xaml.cs file like below.
/// <summary>
/// Interaction logic for UC1.xaml
/// </summary>
public partial class UC1 : UserControl
{
private static List<UC1> _allInstanceOfThisControl = new List<UC1>();
ViewModelUC1 vm = new ViewModelUC1();
public UC1()
{
InitializeComponent();
this.DataContext = vm;
_allInstanceOfThisControl.Add(this);
this.Unloaded += UC1_Unloaded;
}
private void UC1_Unloaded(object sender, RoutedEventArgs e)
{
_allInstanceOfThisControl.Remove(this);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (vm.IsTrue)
{
vm.IsTrue = false;
}
else
{
vm.IsTrue = true;
}
var _allInstanceOfThisControlExceptthis = _allInstanceOfThisControl.Where(s => s != this).ToList();
_allInstanceOfThisControlExceptthis.ForEach(s =>
{
(s.DataContext as ViewModelUC1).IsTrue = vm.IsTrue;
});
}
Hope this will help to achieve your requirement.

How to bind with Mode=OneWay and only propagate changes back on save?

Question
How can I make it so that changes to a note are only propagated back to the list, when the Save button is clicked instead on "lost focus"?
And the Save button should only be enabled when the note has been changed.
UI
The example application looks like this:
The current behaviour is:
Clicking on a note puts its text into the TextBox; that's fine.
The changed text from the TextBox gets written back to the list when the TextBox loses the focus (default binding behaviour); but I only want that to happend when the Save button is clicked.
The Save button is always activated because the CanExecute(object parameter) isn't correctly implemented yet; it should only get activated when the TextBox text is different from the selected note's text.
My research so far
Option 1: Some Internet sources say to bind a different property to the TextBox and to programmatically check whether it is different from the SelectedItem of the ListView. I would have hoped that there was a way without introducing a third property in addition to the already existing ListOfNotes and SelectedNote.
Option 2: Some Internet sources recommend to configure Mode=OneWay so that clicking an item in the ListView updates the TextBox, but not the other way around. This sounds like the solution I would prefer, but I wasn't able to figure out from the code examples how to raise an event programmatically so that the change in the TextBox gets written back to the ListView when the Save button is clicked.
I've found other Stackoverflow questions that seem to be similar to mine, but the answers to those haven't helped me fix the problem:
WPF databinding after Save button click
Code
This example currently does two-way binding on focus lost. How do I need to change it to get the above described behaviour?
https://github.com/lernkurve/WpfBindingOneWayWithSaveButton
MainWindow.xaml
<Window x:Class="WpfBindingOneWayWithSaveButton.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:wpfBindingOneWayWithSaveButton="clr-namespace:WpfBindingOneWayWithSaveButton"
mc:Ignorable="d"
Title="MainWindow" Height="188.636" Width="299.242">
<Window.DataContext>
<wpfBindingOneWayWithSaveButton:MainWindowsViewModel />
</Window.DataContext>
<Grid>
<GroupBox Header="List of notes" HorizontalAlignment="Left" VerticalAlignment="Top" Height="112" Width="129" Margin="0,24,0,0">
<ListView ItemsSource="{Binding ListOfNotes}" SelectedItem="{Binding SelectedNote}" DisplayMemberPath="Text" HorizontalAlignment="Left" Height="79" VerticalAlignment="Top" Width="119" Margin="0,10,-2,0"/>
</GroupBox>
<GroupBox Header="Change selected note" HorizontalAlignment="Left" Margin="134,24,0,0" VerticalAlignment="Top" Height="112" Width="151">
<Grid HorizontalAlignment="Left" Height="89" Margin="0,0,-2,0" VerticalAlignment="Top" Width="141">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40*"/>
<ColumnDefinition Width="101*"/>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding SelectedNote.Text}" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="121" Margin="10,7,0,0" Grid.ColumnSpan="2"/>
<Button Command="{Binding SaveCommand}" Content="Save" HorizontalAlignment="Left" VerticalAlignment="Top" Width="121" Margin="10,35,0,0" Grid.ColumnSpan="2"/>
</Grid>
</GroupBox>
</Grid>
</Window>
MainWindowsViewModel.cs
using System.Collections.ObjectModel;
using System.Windows.Input;
namespace WpfBindingOneWayWithSaveButton
{
public class MainWindowsViewModel
{
public ObservableCollection<Note> ListOfNotes { get; set; }
public Note SelectedNote { get; set; }
public ICommand SaveCommand { get; set; }
public MainWindowsViewModel()
{
ListOfNotes = new ObservableCollection<Note>
{
new Note { Text = "Note 1" },
new Note { Text = "Note 2" }
};
SaveCommand = new SaveCommand(this);
}
}
}
SaveCommand.cs
using System;
using System.Windows.Input;
namespace WpfBindingOneWayWithSaveButton
{
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
// What should go here?
return true;
// Pseudo code
// return (is the TextBox text different from the original note text)
}
public void Execute(object parameter)
{
// What should go here?
// Pseudo code
// Let WPF know that the TextBox text has changed
// Invoke the binding so it propagates the TextBox text back to the list
}
public event EventHandler CanExecuteChanged;
}
}
Note.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfBindingOneWayWithSaveButton
{
public class Note : INotifyPropertyChanged
{
private string text;
public string Text
{
get { return text; }
set
{
text = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Bind the text to the CommandParameter of the SaveButton so it gets passed to the Save method for updating.
<TextBox x:Name="NoteTextBox" Text="{Binding SelectedNote.Text, Mode=OneTime}" ../>
<Button Command="{Binding SaveCommand}"
CommandParameter="{Binding ElementName=NoteTextBox, Path=Text}",
Content="Save" />
and
public bool CanExecute(object parameter)
{
return vm.SelectedNote.Text != parameter as string;
}
public void Execute(object parameter)
{
vm.SelectedNote.Text = parameter as string;
}
Option one is the easiest to implement, you will need to clone the Note object and set it to a separate property.
in your xaml, change your list view to the following so it now binds the SelectedIndex instead of the SelectedItem.
<ListView ItemsSource="{Binding ListOfNotes}" SelectedIndex="{Binding SelectedIndex}" DisplayMemberPath="Text" ...
And change TextBox to the following so it updates the binding as you type
<TextBox Text="{Binding Path=SelectedNote.Text, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" ...
In Note.cs we add the Clone() method.
public class Note : INotifyPropertyChanged
{
public Note Clone()
{
return new Note()
{
Text = this.Text
};
}
//... The rest stays the same
}
In MainWindowsViewModel.cs we add new properties for the SelectedIndex and clone the object when we detect a index has changed. We also need to add INotifyPropertyChanged so we can update the SelectedNote from the codebehind when we do the Clone()
public class MainWindowsViewModel : INotifyPropertyChanged
{
private int _selectedIndex = -1;
private Note _selectedNote;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex.Equals(value))
return;
_selectedIndex = value;
CloneSelectedNote();
}
}
private void CloneSelectedNote()
{
if (SelectedIndex >= 0)
{
SelectedNote = ListOfNotes[SelectedIndex].Clone();
}
else
{
SelectedNote = null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Note SelectedNote
{
get { return _selectedNote; }
set
{
if(Equals(_selectedNote, value))
return;
_selectedNote = value;
OnPropertyChanged();
}
}
//... The rest stays the same
}
In SaveCommand.cs we add the logic for CanExecute and add the subscriptions to CommandManager.RequerySuggested, this automatically makes it requery the CanExecute any time any binding changes. This can be a little ineffecent, if you wanted to you could expose a RaiseCanExecuteChanged() publicly but it would be MainWindowsViewModel responsibility to call it any time vm.SelectedIndex or vm.SelectedNote.Text changed.
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
if (vm.SelectedIndex < 0 || vm.SelectedNote == null)
return false;
return vm.ListOfNotes[vm.SelectedIndex].Text != vm.SelectedNote.Text;
}
public void Execute(object parameter)
{
vm.ListOfNotes[vm.SelectedIndex] = vm.SelectedNote;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
UPDATE: Here is a updated version that does not use CommandManager
MainWindowsViewModel.cs
public class MainWindowsViewModel : INotifyPropertyChanged
{
private int _selectedIndex = -1;
private Note _selectedNote;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex.Equals(value))
return;
_selectedIndex = value;
CloneSelectedNote();
RecheckSaveCommand();
}
}
private void CloneSelectedNote()
{
if (SelectedIndex >= 0)
{
SelectedNote = ListOfNotes[SelectedIndex].Clone();
}
else
{
SelectedNote = null;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public Note SelectedNote
{
get { return _selectedNote; }
set
{
if(Equals(_selectedNote, value))
return;
if (_selectedNote != null)
{
PropertyChangedEventManager.RemoveHandler(_selectedNote, SelectedNoteTextChanged, nameof(Note.Text));
}
_selectedNote = value;
if (_selectedNote != null)
{
PropertyChangedEventManager.AddHandler(_selectedNote, SelectedNoteTextChanged, nameof(Note.Text));
}
OnPropertyChanged();
}
}
private void SelectedNoteTextChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
RecheckSaveCommand();
}
private void RecheckSaveCommand()
{
var command = this.SaveCommand as WpfBindingOneWayWithSaveButton.SaveCommand; //"this." and "WpfBindingOneWayWithSaveButton." are not necessary but I wanted to be explicit.
if (command != null)
{
command.RaiseCanExecuteChanged();
}
}
//...
}
SaveCommand.cs
public class SaveCommand : ICommand
{
private MainWindowsViewModel vm;
public SaveCommand(MainWindowsViewModel vm)
{
this.vm = vm;
}
public bool CanExecute(object parameter)
{
if (vm.SelectedIndex < 0 || vm.SelectedNote == null)
return false;
return vm.ListOfNotes[vm.SelectedIndex].Text != vm.SelectedNote.Text;
}
public void Execute(object parameter)
{
vm.ListOfNotes[vm.SelectedIndex] = vm.SelectedNote;
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
You should not use OneWay but rather an UpdateSourceTrigger of value Explicit. BindingGroups can do this for you though, here's a simple example:
<!-- For change observation -->
<TextBlock Text="{Binding Text}"></TextBlock>
<StackPanel>
<StackPanel.BindingGroup>
<BindingGroup x:Name="EditGroup"></BindingGroup>
</StackPanel.BindingGroup>
<TextBox Text="{Binding Text}"></TextBox>
<Button>
<Button.Command>
<local:CommitGroupCommand BindingGroup="{x:Reference EditGroup}"/>
</Button.Command>
Save
</Button>
</StackPanel>
public class CommitGroupCommand : ICommand
{
public BindingGroup BindingGroup { get; set; }
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
BindingGroup.UpdateSources();
}
}
(You could add a validation rule to your binding that requires the value to be different and use that for the CanExecute implementation.)
Using this method allows you to bind directly to the object you intend to edit, so you don't need to copy around values first.

How to get Enter Key action to call a PCL defined property from WPF using MVVM Cross

My issue is that I wish to press enter after typing a text in a textbox. When I do, I want this to trigger a specific property in my viewmodel which is defined in my PCL (this can not be changed).
I have seen some examples that almost do the similar thing but they only do standard actions such as clear text in textbox or tab to next control and so on. I want this one to interact with a property of my choice.
HeaderView.xaml
<views:MvxWpfView
xmlns:views="clr-namespace:Cirrious.MvvmCross.Wpf.Views;assembly=Cirrious.MvvmCross.Wpf"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:UI="clr-namespace:ProductCatalog.UserInterface.WPF.Bootstrap"
x:Class="ProductCatalog.UserInterface.WPF.Views.HeaderView">
<Grid Height="70" Background="#005287">
<TextBox DataContext="{Binding SearchText}" UI:TextBoxExtension.EnterKey="Search"
Width="120" Height="35" Padding="8" Margin="10" HorizontalAlignment="Right" >
</TextBox>
</Grid></views:MvxWpfView>
HeaderView.xaml.cs
public partial class HeaderView : MvxWpfView
{
public new HeaderViewModel ViewModel
{
get { return (HeaderViewModel) base.ViewModel; }
set { base.ViewModel = value; }
}
public HeaderView()
{
InitializeComponent();
}
}
HeaderViewModel.cs (In PCL)
public class HeaderViewModel : MvxViewModel
{
private string _searchText;
public string SearchText
{
get { return _searchText; }
set
{
_searchText = value;
RaisePropertyChanged(() => SearchText);
}
}
public ICommand Search
{
get
{
return new MvxCommand(() =>
{
SearchItems = new List<string> { "Hey", "Hello", "Hola" };
});
}
}
private IList<string> _searchItems;
public IList<string> SearchItems
{
get { return _searchItems; }
set { _searchItems = value; RaisePropertyChanged(() => SearchItems); }
}
}
TextBoxExtension.cs
public static class TextBoxExtension
{
public static ICommand GetEnterKey(DependencyObject obj)
{
return (ICommand)obj.GetValue(EnterKey);
}
public static void SetEnterKey(DependencyObject obj, ICommand value)
{
obj.SetValue(EnterKey, value);
}
public static readonly DependencyProperty EnterKey =
DependencyProperty.RegisterAttached("EnterKey", typeof(ICommand), typeof(TextBoxExtension), new UIPropertyMetadata(EnterKeyPropertyChanged));
static void EnterKeyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UIElement element = d as UIElement;
if (element == null)
{
return;
}
element.KeyDown += Keydown;
}
static void Keydown(object sender, KeyEventArgs e)
{
if (!e.Key.Equals(Key.Enter))
{
return;
}
UIElement element = sender as UIElement;
if (element != null)
{
ICommand command = GetEnterKey(element);
command.Execute(null);
}
}
}
Here is what happens, when I use the parameter Search into my UI:TextBoxEntension.EnterKey the event is triggered like I want it to, but it can't find my Search property in my ViewModel. When I try using {Binding Search} then the event is never triggered. How can I get the event to trigger my Search property in my ViewModel.
Many thanks
Have you tried using InputBindings:
<TextBox>
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding DoSomething}"/>
</TextBox.InputBindings>
</TextBox>
And in your ViewModel:
DoSomething = new RelayCommand(() => DoSomething(), () => true);
First, you should define your Attached property as
public static readonly DependencyProperty EnterKeyProperty =
DependencyProperty.RegisterAttached("EnterKey", typeof(ICommand), typeof(TextBoxExtension), new UIPropertyMetadata(EnterKeyPropertyChanged));
public static ICommand GetEnterKey(DependencyObject obj)
{
return (ICommand)obj.GetValue(EnterKeyProperty);
}
public static void SetEnterKey(DependencyObject obj, ICommand value)
{
obj.SetValue(EnterKeyProperty, value);
}
This is to differentiate between attached property and CLR property.
Second binding should happen as
<TextBox DataContext="{Binding SearchText}" UI:TextBoxExtension.EnterKey="{Binding Search}"
Width="120" Height="35" Padding="8" Margin="10" HorizontalAlignment="Right" >
</TextBox>

Why is my WPF CheckBox Binding not working?

I'm using MVVM, VS 2008, and .NET 3.5 SP1. I have a list of items, each exposing an IsSelected property. I have added a CheckBox to manage the selection/de-selection of all the items in the list (updating each item's IsSelected property). Everything is working except the IsChecked property is not being updated in the view when the PropertyChanged event fires for the CheckBox's bound control.
<CheckBox
Command="{Binding SelectAllCommand}"
IsChecked="{Binding Path=AreAllSelected, Mode=OneWay}"
Content="Select/deselect all identified duplicates"
IsThreeState="True" />
My VM:
public class MainViewModel : BaseViewModel
{
public MainViewModel(ListViewModel listVM)
{
ListVM = listVM;
ListVM.PropertyChanged += OnListVmChanged;
}
public ListViewModel ListVM { get; private set; }
public ICommand SelectAllCommand { get { return ListVM.SelectAllCommand; } }
public bool? AreAllSelected
{
get
{
if (ListVM == null)
return false;
return ListVM.AreAllSelected;
}
}
private void OnListVmChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "AreAllSelected")
OnPropertyChanged("AreAllSelected");
}
}
I'm not showing the implementation of SelectAllCommand or individual item selection here, but it doesn't seem to be relevant. When the user selects a single item in the list (or clicks the problem CheckBox to select/de-select all items), I have verified that the OnPropertyChanged("AreAllSelected") line of code executes, and tracing in the debugger, can see the PropertyChanged event is subscribed to and does fire as expected. But the AreAllSelected property's get is only executed once - when the view is actually rendered. Visual Studio's Output window does not report any data binding errors, so from what I can tell, the CheckBox's IsSelected property is properly bound.
If I replace the CheckBox with a Button:
<Button Content="{Binding SelectAllText}" Command="{Binding SelectAllCommand}"/>
and update the VM:
...
public string SelectAllText
{
get
{
var msg = "Select All";
if (ListVM != null && ListVM.AreAllSelected != null && ListVM.AreAllSelected.Value)
msg = "Deselect All";
return msg;
}
}
...
private void OnListVmChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "AreAllSelected")
OnPropertyChanged("SelectAllText");
}
everything works as expected - the button's text is updated as all items are selected/desected. Is there something I'm missing about the Binding on the CheckBox's IsSelected property?
Thanks for any help!
I found the problem. It seems a bug existed in WPF 3.0 with OneWay bindings on IsChecked causing the binding to be removed. Thanks to this post for the assistance, it sounds like the bug was fixed in WPF 4.0
To reproduce, create a new WPF project.
Add a FooViewModel.cs:
using System;
using System.ComponentModel;
using System.Windows.Input;
namespace Foo
{
public class FooViewModel : INotifyPropertyChanged
{
private bool? _isCheckedState = true;
public FooViewModel()
{
ChangeStateCommand = new MyCmd(ChangeState);
}
public bool? IsCheckedState
{
get { return _isCheckedState; }
}
public ICommand ChangeStateCommand { get; private set; }
private void ChangeState()
{
switch (_isCheckedState)
{
case null:
_isCheckedState = true;
break;
default:
_isCheckedState = null;
break;
}
OnPropertyChanged("IsCheckedState");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var changed = PropertyChanged;
if (changed != null)
changed(this, new PropertyChangedEventArgs(propertyName));
}
}
public class MyCmd : ICommand
{
private readonly Action _execute;
public event EventHandler CanExecuteChanged;
public MyCmd(Action execute)
{
_execute = execute;
}
public void Execute(object parameter)
{
_execute();
}
public bool CanExecute(object parameter)
{
return true;
}
}
}
Modify Window1.xaml.cs:
using System.Windows;
using System.Windows.Controls.Primitives;
namespace Foo
{
public partial class Window1
{
public Window1()
{
InitializeComponent();
}
private void OnClick(object sender, RoutedEventArgs e)
{
var bindingExpression = MyCheckBox.GetBindingExpression(ToggleButton.IsCheckedProperty);
if (bindingExpression == null)
MessageBox.Show("IsChecked property is not bound!");
}
}
}
Modify Window1.xaml:
<Window
x:Class="Foo.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:Foo"
Title="Window1"
Height="200"
Width="200"
>
<Window.DataContext>
<vm:FooViewModel />
</Window.DataContext>
<StackPanel>
<CheckBox
x:Name="MyCheckBox"
Command="{Binding ChangeStateCommand}"
IsChecked="{Binding Path=IsCheckedState, Mode=OneWay}"
Content="Foo"
IsThreeState="True"
Click="OnClick"/>
<Button Command="{Binding ChangeStateCommand}" Click="OnClick" Content="Change State"/>
</StackPanel>
</Window>
Click on the button a few times and see the CheckBox's state toggle between true and null (not false). But click on the CheckBox and you will see that the Binding is removed from the IsChecked property.
The workaround:
Update the IsChecked binding to be TwoWay and set its UpdateSourceTrigger to be explicit:
IsChecked="{Binding Path=IsCheckedState, Mode=TwoWay, UpdateSourceTrigger=Explicit}"
and update the bound property so it's no longer read-only:
public bool? IsCheckedState
{
get { return _isCheckedState; }
set { }
}

Categories

Resources