.Net Maui MVVM Navigate and Pass Object Between Views - c#

I am struggling to pass an object from one view to another using .Net Maui and a UI independent MVVM pattern.
Most or all MAUI examples I have found to date use and promote MVVM. They also tend to store the View-Models within the UI project and utilize MAUI App Shell navigation directly. This in my opinion and understanding potentially omits some of the benefits of MVVM as View Models would not work with other UI projects.
I have attempted to create a working example* with the View Models, Models and Services being in a separate UI independent project and referenced by the UI project(s) which contain the Views. Sample Project Published to GitHub I would like to update the project with a solution so it can be a working example.
I am specifically stuck where when clicking on a single object from a list of objects it opens a detail view but the object is not passed successfully. In this case my example utilizes a list of customer orders and I am attempting to open the order in a detail view when clicking on an order from the order list.
My problem lies somewhere between the XAML binding to the GoToOrdersCommand, how the command is implemented, and Order object passed to the Order View-Model.
Why do I have both a DelegateCommand and RelayCommand, I am not sure. Different examples I have viewed have used these names, I am not sure if one name is correct. Or if they should be combined into one class. I believe the only significant difference for this example is that RelayCommand accepts an object as a parameter.
Orders View-Model
namespace Orders.Common.ViewModel
{
public class OrdersViewModel : ViewModelBase
{
public ObservableCollection<Order> Orders { get; } = new();
private readonly OrderDataProvider _orderDataProvider;
private IOrderNavigation _navigationService;
public RelayCommand<Order> GoToOrdersCommand { get; }
private Order _order;
public OrdersViewModel(IOrderNavigation navigationService)
{
Orders = new ObservableCollection<Order>();
_orderDataProvider = new OrderDataProvider();
_navigationService = navigationService;
_order = new Order();
GoToOrdersCommand = new RelayCommand<Order>((order) => OrderDetails(_order));
Load();
}
public void Load()
{
var orders = _orderDataProvider.GetAllOrders();
Orders.Clear();
foreach (var order in orders)
Orders.Add(order);
}
public void OrderDetails(Order order)
{
if (order == null)
return;
_navigationService.NavigateToOrderAsync(order);
}
}
}
Order View-Model
public class OrderViewModel : ViewModelBase
{
private Order _order;
public Order Order
{
get => _order;
set
{
if (_order != value)
{
_order = value;
RaisePropertyChanged();
}
}
}
private OrderDataProvider _orderDataProvider = new OrderDataProvider();
public DelegateCommand SaveCommand { get; }
public ICommand SaveOrder { get; set; }
public OrderViewModel()
{
_order = new Order() { OrderID = -1, Customer = "", OrderDate = DateTime.Now };
SaveCommand = new DelegateCommand(Save, () => CanSave);
}
public OrderViewModel(Order order)
{
_order = order;
SaveCommand = new DelegateCommand(Save, () => CanSave);
}
public bool CanSave => !string.IsNullOrEmpty(CustomerName) && CustomerName.Length >= 3;
public void Save()
{
throw new NotImplementedException();
}
public int OrderID
{
get => _order.OrderID;
set
{
if (_order.OrderID != value)
{
_order.OrderID = value;
RaisePropertyChanged();
}
}
}
public string CustomerName
{
get => _order.Customer;
set
{
if (_order.Customer != value)
{
_order.Customer = value;
RaisePropertyChanged();
RaisePropertyChanged(nameof(CanSave));
SaveCommand.RaiseCanExecuteChanged();
}
}
}
public DateTime OrderDate
{
get => _order.OrderDate;
set
{
if (_order.OrderDate != value)
{
_order.OrderDate = value;
RaisePropertyChanged();
}
}
}
}
Inherited UI Specific Navigation
namespace TestOrders.Navigate
{
public class OrderNavigationService : IOrderNavigation
{
public void NavigateToOrdersAsync()
{
Shell.Current.GoToAsync(nameof(OrdersPage));
}
public void NavigateToOrderAsync(Order order)
{
Shell.Current.GoToAsync(nameof(OrderPage), true, new Dictionary<string, object> { { "Order", order } });
}
}
}
RelayCommand
namespace Orders.Common.ViewModel.Command
{
public class RelayCommand<T> : ICommand
{
private Action<object> _execute;
private Func<object, bool> _canExecute;
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
}
}
Orders View
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TestOrders.Pages.OrdersPage"
Title="OrdersPage"
xmlns:model="clr-namespace:Orders.Common.Model;assembly=Orders.Common"
xmlns:viewmodel="clr-namespace:Orders.Common.ViewModel;assembly=Orders.Common"
x:DataType="viewmodel:OrdersViewModel">
<VerticalStackLayout>
<Label Text="Orders" HorizontalOptions="Center"/>
<CollectionView
ItemsSource="{Binding Orders}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="model:Order">
<Grid Padding="10">
<Frame HeightRequest="70">
<Frame.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:OrdersViewModel}}, Path=GoToOrdersCommand}"
CommandParameter="{Binding .}"/>
</Frame.GestureRecognizers>
<Grid Padding="0" ColumnDefinitions="20,*">
<Label Text="{Binding OrderID}" Grid.Column="0"/>
<Label Text="{Binding Customer}" Grid.Column="1"/>
</Grid>
</Frame>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</ContentPage>
*Being new to .Net Maui, XAML based UI's, MVVM patterns and inexperienced with additional topics such as interfaces and delegates there are likely best practices or coding conventions I have missed. I am also trying to avoid utilizing the MVVM community toolkit or other toolkits until I have a better understanding of MVVM and how the interfaces and commands function.

I believe that this
GoToOrdersCommand = new RelayCommand<Order>((order) => OrderDetails(_order));
should be
GoToOrdersCommand = new RelayCommand<Order>((order) => OrderDetails(order));
_order is a new instance that you create on the preceding line, NOT the instance being passed in from the view's binding

Change to OrdersViewModel Constructor
public OrdersViewModel(IOrderNavigation navigationService)
{
Orders = new ObservableCollection<Order>();
_orderDataProvider = new OrderDataProvider();
_navigationService = navigationService;
GoToOrdersCommand = new RelayCommand<Order>((order) => OrderDetails((Order)order));
GoToNewOrderCommand = new DelegateCommand(NewOrder); // Additional navigate without object
Load();
}
Additional Command method to navigate to a New/Empty Order Detail for reference added to the OrdersViewModel
private void NewOrder()
{
_navigationService.NavigateToOrderAsync();
}
Updated Order Navigation
public class OrderNavigationService : IOrderNavigation
{
public void NavigateToOrdersAsync()
{
Shell.Current.GoToAsync(nameof(OrdersPage));
}
public void NavigateToOrderAsync(Order order)
{
Shell.Current.GoToAsync(nameof(OrderPage), true, new Dictionary<string, object> { { "Order", order } });
}
public void NavigateToOrderAsync()
{
Shell.Current.GoToAsync(nameof(OrderPage));
}
}
OrderPage View Code Behind File
Added QueryProperty, a field and method to create an OrderViewModel if an Order object is provided.
[QueryProperty(nameof(order), "Order")]
public partial class OrderPage : ContentPage
{
public Order order
{
set
{
Load(value);
}
}
public OrderPage()
{
InitializeComponent();
BindingContext = new OrderViewModel();
}
private void Load(Order order)
{
if (order != null)
BindingContext = new OrderViewModel(order);
}
}

Related

How to implement Inversion of control c# - wpf

I'm learning on how to implement inversion of control in the wpf application through the below link
https://www.codeguru.com/columns/experts/implementing-the-inversion-of-control-pattern-in-c.htm
The intent of the application is to load the data from table to combobox. The code works fine without any issue. So as a part of learning ooad design principle I thought below is the perfect case on which the highlevel module (Viewmodel) depend upon lowerlevel module (populatetab2combobox). I believe here we can apply dependency inversion. on reading online most of them started with inversion of control as a first step towards implementing Dependency inversion.
I just want to iterate that I don't have prior experience in oops principle. I might be wrong in my assumption. Please correct me if I'm wrong.
Window.xaml :
<ComboBox ItemsSource="{Binding populatecombobox.modeltogetusername}" Width="155" Margin="0,-20,-180,137">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding username}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
View Model :
Code without implementing inversion of control(This works fine without any issue):
//Class to populate the combobox
public class populatetab2combobox
{
public ObservableCollection<comboboxdata> modeltogetusername { get; set; }
public void getdatausinglinq()
{
using (Operations_Productivity_ToolEntities context = new Operations_Productivity_ToolEntities())
{
var a1 = from t1 in context.Test_ImportedAuditdata
select t1;
if (modeltogetusername == null)
modeltogetusername = new ObservableCollection<comboboxdata>();
foreach (var a in a1.GroupBy(x => x.username).Select(x => x.FirstOrDefault()))
{
modeltogetusername.Add(new comboboxdata
{
username = a.username.ToString()
});
}
}
}
}
public class ViewModel: INotifyPropertyChanged {
/** You can see that I'm calling viewModel class is depended with
populatetab2combobox. I believe this is the perfect case for implementing
Inversion of control **/
populatetab2combobox_populatecombobox = new populatetab2combobox();
private PopulateDatagrid _populatedatagridwithobservablecollection = new PopulateDatagrid();
private Loadfileintodatabase loaddata = new Loadfileintodatabase();
public PopulateDatagrid Populatedatagridwithobservablecollection {
get {
return _populatedatagridwithobservablecollection;
}
set {
if (value != _populatedatagridwithobservablecollection) {
_populatedatagridwithobservablecollection = value;
OnPropertyChanged("Populatedatagridwithobservablecollection");
}
}
}
DataModel dm = new DataModel();
public ViewModel() {
_populatecombobox.getdatausinglinq();
DoSomeThingCmd = new RelayCommand(o = >search());
_populatedatagridwithobservablecollection.getdatausinglinq();
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
Code with implementation of inversion of control(The issue is combobox is empty when i run when IOC implemented):
public interface INofificationAction
{
void getdatausinglinq();
}
public class populatetab2combobox : INofificationAction
{
public ObservableCollection<comboboxdata> modeltogetusername { get; set; }
public void getdatausinglinq()
{
using (Operations_Productivity_ToolEntities context = new Operations_Productivity_ToolEntities())
{
var a1 = from t1 in context.Test_ImportedAuditdata
select t1;
if (modeltogetusername == null)
modeltogetusername = new ObservableCollection<comboboxdata>();
foreach (var a in a1.GroupBy(x => x.username).Select(x => x.FirstOrDefault()))
{
modeltogetusername.Add(new comboboxdata
{
username = a.username.ToString()
});
}
}
}
}
public class implementingabstraction
{
INofificationAction _an;
public implementingabstraction(INofificationAction action)
{
this._an = action;
}
public void getdatausinglinq()
{
_an.getdatausinglinq();
}
}
public class ViewModel: INotifyPropertyChanged
{
INofificationAction getdata123 = new populatetab2combobox();
populatetab2combobox _populatecombobox = new populatetab2combobox();
public populatetab2combobox populatecombobox {
get {
return _populatecombobox;
}
set {
if (value != _populatecombobox) {
_populatecombobox = value;
OnPropertyChanged("populatetab2combobox");
}
}
}
public RelayCommand DoSomeThingCmd {
get;
set;
}
DataModel dm = new DataModel();
public ViewModel() {
implementingabstraction abs = new implementingabstraction(getdata123);
abs.getdatausinglinq();
DoSomeThingCmd = new RelayCommand(o = >search());
_populatedatagridwithobservablecollection.getdatausinglinq();
}
}
questions:
1) since I'm very new to oops based approach.Whether the above usecase is correct.
2) The above code is not working correctly. Can you please point out the mistake in my approach.

Can't bind to a member of a Model from a ViewModel

MyProduct is the model that has HasError boolean property (with OnPropertyChanged ...) that can change.
MyProductDialogViewModel is:
class ProductDialogViewModel : Notifier
{
public ProductDialogViewModel() { }
public MyProduct Product { get; set; }
public bool HasError
{
get { return Product.HasError; }
}
}
I have assigned MyProductDialogViewModel instance to BaseContentControl.DataContext to inflate a ContentControl.
This View can be inflated with different ViewModels all having HasError property using template binding.
<ContentControl x:Name="BaseContentControl" Content="{Binding}" ... >
Then I try to extract informations directly from its DataContext.
This don't work:
<Label Content="{Binding ElementName=BaseContentControl, Path=DataContext.HasError}"/>
But this works perfectly.
<Label Content="{Binding ElementName=BaseContentControl, Path=DataContext.Product.HasError}"/>
I tought it ca be a notifiy problem in the ViewModel so I have changed to this:
class ProductDialogViewModel : Notifier
{
public ProductDialogViewModel() { }
public MyProduct Product { get; set; }
public bool HasError
{
get { return Product.HasError; }
set
{
if (Product.HasError != value)
{
Product.HasError = value;
OnPropertyChanged("HasError");
}
}
}
}
but to no avail (in fact the set method is never called so it never notifies).
I don't want to directly refer to the specific Model instance cause the View can be inflated with different ViewModels.
How can I do ?
Thanks
You have to propagate the PropertyChanged event of MyProduct, i.e. subscribe to it and invoke OnPropertyChanged(nameof(HasError)) if HasError property of MyProduct being changed:
public class ProductDialogViewModel : Notifier
{
public ProductDialogViewModel() { }
private MyProduct _product = null;
public MyProduct Product
{
get { return _product; }
set
{
if (_product!=null)
{
_product.PropertyChanged -= Product_PropertyChanged;
}
_product = value;
if (_product != null)
{
_product.PropertyChanged += Product_PropertyChanged;
}
}
}
private void Product_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName==nameof(MyProduct.HasError))
{
OnPropertyChanged(nameof(HasError));
}
}
public bool HasError => Product.HasError;
}

Save data to Collection and Display using MVVM WPF

I have my Model class which implements INotifyPropertyChanged.
My View has 5 TextBox, 2 Button and a ListView to display grid.
In my ViewModel I was previously adding default values to the ObservableCollection of my Model class and displaying it to a ListView.
The button implementation is done using ICommand and RelayCommand.
Now I want to add data to the ObservableCollection from the user from the UI TextBox. How can I achieve that? The UI TextBox hasbindings with properties of the Model class.
My View
<ListView Name="UserGrid" Grid.Row="1" Margin="4,178,12,13" ItemsSource="{Binding UserDatas}" >
<TextBox Grid.Row="1" Height="23" HorizontalAlignment="Left" Margin="80,7,0,0" Name="txtUserId" VerticalAlignment="Top" Width="178" Text="{Binding UserId}" />
<TextBox Grid.Row="1" Height="23" HorizontalAlignment="Left" Margin="80,35,0,0" Name="txtFirstName" VerticalAlignment="Top" Width="178" Text="{Binding FirstName}" />
Like these there are 5 TextBoxes
The Model class:
public class User : INotifyPropertyChanged
{
private int userId;
private string firstName;
public int UserId
{
get
{
return userId;
}
set
{
userId = value;
RaisePropertyChanged("UserId");
}
}
}
The ViewModel:
public class UsersViewModel:INotifyPropertyChanged
{
private ObservableCollection<User> userDatas;
public ObservableCollection<User> UserDatas
{
get
{
if (userDatas == null)
{
userDatas = new ObservableCollection<User>();
}
return userDatas;
}
set
{
userDatas = value;
RaisePropertyChanged("UserDatas");
}
}
}
private CommandBase _LoadCommand;
public ICommand LoadCommand
{
get
{
if (this._LoadCommand == null)
this._LoadCommand = new CommandBase(LoadData);
return this._LoadCommand;
}
}
private void LoadData(object obj)
{
//What need to be done here to access the textboxes of UI which are binded to User.cs class.
User newUser = new User();
UserDatas.Add(newUser);
}
Now what I need to write in the LoadData method to take inputfrom textboxes from UI and store it in my ObservableCollection
There are several things you can do. Most obvious, is to have a "Add New" Command, which creates a new empty object and stores it in a CurrentUser or SelectedUser property.
This property is bound to the Template's (or Form's) context. You'd have 3 commands (Add New User, Save User, Cancel to cancel addition of a new user creation).
For example
public class UsersViewModel : INotifyPropertyChanged
{
public UsersViewModel()
{
UserDatas = new ObservableCollection<User>();
AddNewUserCommand = new RelayCommand(AddNewUser, param => !this.IsNewUser);
SaveUserCommand = new RelayCommand(SaveUser);
CancelNewUserCommand = new RelayCommand(CancelNewUser, param => this.IsNewUser);
}
private ObservableCollection<User> userDatas;
public ObservableCollection<User> UserDatas
{
get { return userDatas; }
set
{
userDatas = value;
RaisePropertyChanged("UserDatas");
}
}
private User selectedUser;
public User SelectedUser
{
get { return selectedUser; }
set
{
selectedUser = value;
RaisePropertyChanged("SelectedUser");
RaisePropertyChanged("IsNewUser");
}
}
public bool IsNewUser
{
get
{
if(SelectedUser==null)
return false;
return SelectedUser.UserId == 0;
}
}
public ICommand AddNewUserCommand { get; private set; }
public ICommand CancelNewUserCommand { get; private set; }
public ICommand SaveUserCommand { get; private set; }
private void AddNewUser()
{
SelectedUser = new User();
}
private void SaveUser()
{
// Just in case of concurency
var newUser = SelectedUser;
if(newUser == null)
{
return;
}
var isNewUser = newUser.UserId == 0;
// Persist it to the database
this.userRepository.Add(newUser);
this.userRepository.SaveChanges();
// If all worked well, add it to the observable collection
if(isNewUser)
{
// Only add if new, otherwise it should be already in the collection
UserDatas.Add(newUser)
}
}
}
But again, it's very discouraged to work directly on the model and bind it to the View. You should also create a ViewModel for your User and put validation (implement the IDataErrorInfo interface on the UserViewModel) in there and handling of state, for example tracking if the UserViewModel is dirty (i.e. data was changed).
All these are presentation concerns and not business logic, so they belong to a ViewModel and not to the Model itself.

ViewModel data not being found

My project ViewModel elements are not being found. I'm trying to implement a ViewModel within my WPF Usercontrol. However, the binding isn't working properly and there appears to be no data. I'm trying to create a ViewModel to interact with, putting generic string arrays into, and various other bits of data.
MainWindow.xaml - (Usercontrol declaration)
<panels:FilterLister Grid.Column="0" x:Name="filter1FilterLister" />
MainWindows.cs - (Within the constructor, call to usercontrol
filter1FilterLister.Initialise(typeof(Genre));
FilterListViewModel.cs
public class FilterListViewModel
{
MyEntities context = new MyEntities();
ObservableCollection<string> entries = new ObservableCollection<string>();
public Type SelectedType;
private string p_TypeName;
public string TypeName
{
get { return p_TypeName; }
set {
//p_TypeName = value;
p_TypeName = SelectedType.Name.ToString();
}
}
public FilterListViewModel() { }
public FilterListViewModel(Type selectedType)
{
if (selectedType == typeof(Artist))
{
returnedArray = Artist.ReturnArtistNames(context);
}
// put together ObservableCollection
foreach (var str in returnedArray)
{
entries.Add(str);
}
SelectedType = selectedType;
}
}
FilterLister.xaml
<Label Name="labelToBind" Content="{Binding TypeName}" Grid.Row="0" />
FilterLister.cs
public partial class FilterLister : UserControl
{
FilterListViewModel filterListViewModel;
private MyEntities context;
public FilterLister()
{
InitializeComponent();
context = new MyEntities();
}
public void Initialise(Type objectType)
{
filterListViewModel = new FilterListViewModel(objectType);
this.DataContext = filterListViewModel;
}
}
Based on your code, TypeName is null so you saw nothing on the Label. From your code, I think you want to describe like:
public string TypeName
{
get{ return SelectedType.Name.ToString();}
}
As deryck suggested, you should add INotifyPropertyChanged interface for notification, but it should not affect binding at first time. If you believe ViewModel's data is correct but not populated on UI, you should check DataContext and Binding.
You've missed implement the INotifyPropertyChanged interface in your ViewModel, it's needed to the binded property can send "refresh message" to a UI.
Here is the interface, and how you can implement this:
http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged.aspx
public class FilterListViewModel : INotifyPropertyChanged
{
MyEntities context = new MyEntities();
ObservableCollection<string> entries = new ObservableCollection<string>();
public Type SelectedType;
private string p_TypeName;
public string TypeName
{
get { return p_TypeName; }
set {
//p_TypeName = value;
p_TypeName = SelectedType.Name.ToString();
NotifyPropertyChanged();
}
}
public FilterListViewModel() { }
public FilterListViewModel(Type selectedType)
{
if (selectedType == typeof(Artist))
{
returnedArray = Artist.ReturnArtistNames(context);
}
// put together ObservableCollection
foreach (var str in returnedArray)
{
entries.Add(str);
}
SelectedType = selectedType;
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}

Filter items of a ListBox based on the text of a TextBox using only XAML in WPF

I currently have a ListBox binded to a collection of items. As the collection is big we want to filter the items being shown based on the text entered on a TextBox.
What I'm asking is if this is possible to implement using only XAML, I don't want to modify the collection of items, I would like to modify the Visibility of each of the items based on the filter.
Hope its clear,
thanks!
Like CodeNaked and devdigital told you CollectionViewSource/CollectionView/ICollectionView
are the keys to your goal
It's a MVVM patter but this is a View only related problem so I don't
want this code at the ViewModel.
thats not the right way because the View only shows what she get´s but shouldn´t modifi
so it should/must be your ViewModel who handel changes
so now some code snips:
public class myVM
{
public CollectionViewSource CollViewSource { get; set; }
public string SearchFilter
{
get;
set
{
if(!string.IsNullOrEmpty(SearchFilter))
AddFilter();
CollViewSource.View.Refresh(); // important to refresh your View
}
}
public myVM(YourCollection)
{
CollViewSource = new CollectionViewSource();//onload of your VM class
CollViewSource.Source = YourCollection;//after ini YourCollection
}
}
Xaml Snip:
<StackPanel>
<TextBox Height="23" HorizontalAlignment="Left" Name="tB" VerticalAlignment="Top"
Width="120" Text="{Binding SearchFilter,UpdateSourceTrigger=PropertyChanged}" />
<DataGrid Name="testgrid" ItemsSource="{Binding CollViewSource.View}"/>
</StackPanel>
Edit i forgot the Filter
private void AddFilter()
{
CollViewSource.Filter -= new FilterEventHandler(Filter);
CollViewSource.Filter += new FilterEventHandler(Filter);
}
private void Filter(object sender, FilterEventArgs e)
{
// see Notes on Filter Methods:
var src = e.Item as YourCollectionItemTyp;
if (src == null)
e.Accepted = false;
else if ( src.FirstName !=null && !src.FirstName.Contains(SearchFilter))// here is FirstName a Property in my YourCollectionItem
e.Accepted = false;
}
You can use the CollectionViewSource to apply filtering, another example can be found here and here.
You can do this with a CollectionViewSource. You wouldn't want to do this completely in XAML, as it would be much easier to test this if the filtering code is in your view model (assuming an MVVM design pattern).
There is no way to accomplish this in XAML only. But there are other two ways:
1) using converter
<TextBox x:Name="text"/>
<ListBox Tag="{Binding ElementName=text}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Visibility" Value="{Binding RelativeSource={RelativeSource AncestorType=ListBox},Path=Tag, Converter={StaticResource filterLogicConverter}}"/>
</Style>
</ListBox.ItemContainerStyle>
<LixtBox/>
2) better and more natural way is to use CollectionView.Filter property. It doesn't modify an underlying collection.
var collectionView = CollectionViewSource.GetDefaultView(your_collection);
collectionView.Filter = filter_predicate
The only thing XAML really does is encapsulating logic in a declarative fashion. Using markup extensions you can do quite a lot, here's an example:
<StackPanel>
<StackPanel.Resources>
<CollectionViewSource x:Key="items" Source="{Binding Data}">
<CollectionViewSource.Filter>
<me:Filter>
<me:PropertyFilter PropertyName="Name"
RegexPattern="{Binding Text, Source={x:Reference filterbox}}" />
</me:Filter>
</CollectionViewSource.Filter>
</CollectionViewSource>
</StackPanel.Resources>
<TextBox Name="filterbox" Text="Skeet">
<TextBox.TextChanged>
<me:ExecuteActionsHandler ThrowOnException="false">
<me:CallMethodAction>
<me:CallMethodActionSettings MethodName="Refresh"
TargetObject="{Binding Source={StaticResource items}}" />
</me:CallMethodAction>
</me:ExecuteActionsHandler>
</TextBox.TextChanged>
</TextBox>
<!-- ListView here -->
</StackPanel>
(Note that this works but it will trip every GUI designer, also there is no IntelliSense for the events as they usually are not set via element syntax.)
There are several markup extensions here of which two create handlers and one creates an action:
FilterExtension
ExecuteActionsHandlerExtension
CallMethodActionExtension
The extensions look like this:
[ContentProperty("Filters")]
class FilterExtension : MarkupExtension
{
private readonly Collection<IFilter> _filters = new Collection<IFilter>();
public ICollection<IFilter> Filters { get { return _filters; } }
public override object ProvideValue(IServiceProvider serviceProvider)
{
return new FilterEventHandler((s, e) =>
{
foreach (var filter in Filters)
{
var res = filter.Filter(e.Item);
if (!res)
{
e.Accepted = false;
return;
}
}
e.Accepted = true;
});
}
}
public interface IFilter
{
bool Filter(object item);
}
Quite straightforward, just loops through filters and applies them. Same goes for the ExecuteActionsHandlerExtension:
[ContentProperty("Actions")]
public class ExecuteActionsHandlerExtension : MarkupExtension
{
private readonly Collection<Action> _actions = new Collection<Action>();
public Collection<Action> Actions { get { return _actions; } }
public bool ThrowOnException { get; set; }
public ExecuteActionsHandlerExtension()
{
ThrowOnException = true;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return new RoutedEventHandler((s, e) =>
{
try
{
foreach (var action in Actions)
{
action.Invoke();
}
}
catch (Exception)
{
if (ThrowOnException) throw;
}
});
}
}
Now the last extension is a bit more complicated as it actually needs to do something concrete:
[ContentProperty("Settings")]
public class CallMethodActionExtension : MarkupExtension
{
//Needed to provide dependency properties as MarkupExtensions cannot have any
public CallMethodActionSettings Settings { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
return new Action(() =>
{
bool staticCall = Settings.TargetObject == null;
var argsCast = Settings.MethodArguments.Cast<object>();
var types = argsCast.Select(x => x.GetType()).ToArray();
var args = argsCast.ToArray();
MethodInfo method;
if (staticCall)
{
method = Settings.TargetType.GetMethod(Settings.MethodName, types);
}
else
{
method = Settings.TargetObject.GetType().GetMethod(Settings.MethodName, types);
}
method.Invoke(Settings.TargetObject, args);
});
}
}
public class CallMethodActionSettings : DependencyObject
{
public static readonly DependencyProperty MethodNameProperty =
DependencyProperty.Register("MethodName", typeof(string), typeof(CallMethodActionSettings), new UIPropertyMetadata(null));
public string MethodName
{
get { return (string)GetValue(MethodNameProperty); }
set { SetValue(MethodNameProperty, value); }
}
public static readonly DependencyProperty TargetObjectProperty =
DependencyProperty.Register("TargetObject", typeof(object), typeof(CallMethodActionSettings), new UIPropertyMetadata(null));
public object TargetObject
{
get { return (object)GetValue(TargetObjectProperty); }
set { SetValue(TargetObjectProperty, value); }
}
public static readonly DependencyProperty TargetTypeProperty =
DependencyProperty.Register("TargetType", typeof(Type), typeof(CallMethodActionSettings), new UIPropertyMetadata(null));
public Type TargetType
{
get { return (Type)GetValue(TargetTypeProperty); }
set { SetValue(TargetTypeProperty, value); }
}
public static readonly DependencyProperty MethodArgumentsProperty =
DependencyProperty.Register("MethodArguments", typeof(IList), typeof(CallMethodActionSettings), new UIPropertyMetadata(null));
public IList MethodArguments
{
get { return (IList)GetValue(MethodArgumentsProperty); }
set { SetValue(MethodArgumentsProperty, value); }
}
public CallMethodActionSettings()
{
MethodArguments = new List<object>();
}
}
All of these snippets are just quick drafts to demonstrate how one could approach this. (A draft for the property filter implementation can be found in this answer.)
Use a data trigger on some property of the item in the collectin and you can do it all in xaml.

Categories