I have an AutoCompleteBox inside the CellEditingTemplate for a DataGridTemplateColumn and I am trying to auto-focus on it after a certain number of characters have been entered in a previous column.
I have been able to get the focus to shift and the caret to set appropriately using the BeginInvoke method (described here) but only if the control is a TextBox. when using this method with an AutoCompleteBox the caret does not set nor does the control appear to gain focus.
I attempted to set focus on the AutoCompleteBox manually by obtaining a reference to it inside the setCaretInCurrentCell method and invoking focus but that didn't work either.
I really wannt AutoCompleteBox functionality in this column but the datagrid needs to be optimized for data entry which meeans if the users can't tab or automatically be taken to the next field its a show stopper.
Thanks.
I might be going with the oddest suggestion on this one, but instead of trying to get inside the grid / control tree and be explicit with things, use the Current Column setting.
Ripping off the data template sample in the help files the XAML and basiccode are as follows:
It's not perfect by a long margin in that:
it is just hacking together moving from column 0 to 1 based on the pressing of a space, your requirement on the condition to move focus is more complex.
I've not set up two way binding so the edit persists etc. just looked at how to move column.
But when it runs, it does respond to a space and move the cursor into the autocomplete box, highlights the text and if I start typing the autocomplete drop down activates. So in principle, setting the current column seems to provide the behaviour you are after.
A.
Code:
<UserControl x:Class="SilverlightApplication2.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
xmlns:input="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input"
xmlns:toolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
xmlns:system="clr-namespace:System;assembly=mscorlib"
Width="400" Height="300">
<ScrollViewer VerticalScrollBarVisibility="Auto" BorderThickness="0">
<StackPanel Margin="10,10,10,10">
<TextBlock Text="DataGrid with template column and custom alternating row backgrounds:"/>
<data:DataGrid x:Name="dataGrid5"
Height="125" Margin="0,5,0,10"
AutoGenerateColumns="False"
RowBackground="Azure"
AlternatingRowBackground="LightSteelBlue">
<data:DataGrid.Columns>
<!-- Address Column -->
<data:DataGridTemplateColumn Header="Address" Width="300">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Padding="5,0,5,0" Text="{Binding Address}"/>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Padding="5,0,5,0" Text="{Binding Address}"/>
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
<!-- Name Column -->
<data:DataGridTemplateColumn Header="Name">
<data:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Padding="5,0,5,0"
Text="{Binding FirstName}"/>
</StackPanel>
</DataTemplate>
</data:DataGridTemplateColumn.CellTemplate>
<data:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<input:AutoCompleteBox Padding="5,0,5,0"
Text="{Binding FirstName}">
<input:AutoCompleteBox.ItemsSource>
<toolkit:ObjectCollection>
<system:String>January</system:String>
<system:String>February</system:String>
<system:String>March</system:String>
<system:String>April</system:String>
<system:String>May</system:String>
<system:String>June</system:String>
<system:String>July</system:String>
<system:String>August</system:String>
<system:String>September</system:String>
<system:String>October</system:String>
<system:String>November</system:String>
<system:String>December</system:String>
</toolkit:ObjectCollection>
</input:AutoCompleteBox.ItemsSource>
</input:AutoCompleteBox>
</StackPanel>
</DataTemplate>
</data:DataGridTemplateColumn.CellEditingTemplate>
</data:DataGridTemplateColumn>
</data:DataGrid.Columns>
</data:DataGrid>
<Button Content="test"></Button>
</StackPanel>
</ScrollViewer>
</UserControl>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace SilverlightApplication2
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
// Set the ItemsSource to autogenerate the columns.
dataGrid5.ItemsSource = Customer.GetSampleCustomerList();
dataGrid5.KeyDown += new KeyEventHandler(dataGrid5_KeyDown);
}
void dataGrid5_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
{
// move to next cell and start editing
DataGrid grd = (DataGrid)sender;
if (grd.CurrentColumn.DisplayIndex == 0)
{
// move to column 1 and start the edit
grd.CurrentColumn = grd.Columns[1];
}
}
}
}
public class Customer
{
public String FirstName { get; set; }
public String LastName { get; set; }
public String Address { get; set; }
public Boolean IsNew { get; set; }
// A null value for IsSubscribed can indicate
// "no preference" or "no response".
public Boolean? IsSubscribed { get; set; }
public Customer(String firstName, String lastName,
String address, Boolean isNew, Boolean? isSubscribed)
{
this.FirstName = firstName;
this.LastName = lastName;
this.Address = address;
this.IsNew = isNew;
this.IsSubscribed = isSubscribed;
}
public static List<Customer> GetSampleCustomerList()
{
return new List<Customer>(new Customer[4] {
new Customer("A.", "Zero",
"12 North",
false, true),
new Customer("B.", "One",
"34 West",
false, false),
new Customer("C.", "Two",
"56 East",
true, null),
new Customer("D.", "Three",
"78 South",
true, true)
});
}
}
}
Related
Very new to WPF, followed along some youtube tutorials utilizing MVVM and Caliburn Micro, and had the ActiveItem working properly so I am sure my set up is sound. Since completing that tutorial I have tried to customize my app and my intended feature with ActiveItem does not work. App launches with no errors and my login successfully retrieves information from the database and logs me in and displays my name in the bottom left corner.
Here is the intended function:
I have my ShellView as the login page, and after the user signs in it triggers the active item to be the space behind the login rectangle. This code triggers but the login stays visible. It triggers WelcomeViewModel which is just a rectangle with a blue background. The border login doesn't actually disappear, it should just get covered up by the active window (at least for now).
Clicking the space brings up the ContentControl so I am pretty sure it is high-z order, but something is still not right. If I double click the space, it selects the ContentControl and shows its background color appearing on top of the log in which is desired, but not what I see when the application runs.
Xaml for ShellView:
<Window x:Class="ProductivityTool.Views.ShellView"
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:local="clr-namespace:ProductivityTool.Views"
xmlns:model="clr-namespace:ProductivityTool.ViewModels"
mc:Ignorable="d"
d:DataContext="{x:Type model:ShellViewModel}"
Title="Productivity Tool" Height="900" Width="1200" MinWidth="900" MinHeight="700">
<Grid Background="Transparent" RenderTransformOrigin="0.487,0.497">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="*" />
<RowDefinition Height="60" />
</Grid.RowDefinitions>
<!--Row 1-->
<!--
<TextBlock Text="{Binding Path=FullName, Mode=OneWay}" Grid.Row="1" Grid.Column="0"></TextBlock>
-->
<!--Oneway only pulls from the property, but never overwrote the property because it is one way-->
<!--Row 2-->
<!--
<TextBox MinWidth="100" Grid.Row="2" Grid.Column="0" x:Name="FirstName" HorizontalAlignment="Left"></TextBox>
<TextBox MinWidth="100" Grid.Row="2" Grid.Column="0" x:Name="LastName" HorizontalAlignment="Right"></TextBox>
-->
<!--Row 3-->
<!--OnewaytoSource only overwrites the property, opposite of OneWay-->
<!--
<ComboBox Grid.Row="3" Grid.Column="0" x:Name="People"
SelectedItem="{Binding Path=SelectedPerson, Mode=OneWayToSource}"
DisplayMemberPath="FirstName"
HorizontalAlignment="Left" Width="100"/>
<TextBlock Grid.Column="0" Grid.Row="3" x:Name="SelectedPerson_LastName" HorizontalAlignment="Right"/>
-->
<!--Row 4-->
<!--
<Button x:Name="ClearText" Grid.Row="4" Grid.Column="0">Clear Text</Button>
-->
<!--Row 5-->
<StackPanel x:Name="StackPanel" Orientation="Vertical" Grid.Column="0" Grid.Row="1" Background="Blue">
<Button x:Name="Phonebook" HorizontalAlignment="Center" Width="auto" Margin="10" Click="Phonebook_Click">Phonebook</Button>
<Button x:Name="LoadPageTwo" HorizontalAlignment="Center" Width="auto">RCPS</Button>
</StackPanel>
<!--content control is the main window of the ShellView, different pages are just different active items-->
<Border Margin="272,208,-401,255" Padding="5" BorderThickness="1" Background="Gray" CornerRadius="20" Grid.Column="1" Grid.Row="1">
</Border>
<TextBox x:Name="textBoxEmail" Grid.Column="1" HorizontalAlignment="Left" Height="30" Margin="498,246,-347,433" Grid.Row="1" VerticalAlignment="Center" Width="191"/>
<TextBox x:Name="textBoxUID" Grid.Column="1" HorizontalAlignment="Left" Height="30" Margin="498,327,-346,352" Grid.Row="1" VerticalAlignment="Center" Width="191"/>
<TextBlock FontSize="15" Margin="344,245,-143,433" Text="Employee Number:" Grid.Column="1" Grid.Row="1" HorizontalAlignment="Center" Padding="3" Height="31"/>
<TextBlock FontSize="15" Margin="363,327,-125,352" Text="Email:" Grid.Column="1" Grid.Row="1" HorizontalAlignment="Center" Padding="3" Height="30" />
<Button x:Name="Loginbtn" Grid.Column="1" Margin="321,391,-347,278" Grid.Row="1" Content="LOGIN" Click="LoginBtn_Click">
</Button>
<Label x:Name="lblSignedInAs" HorizontalContentAlignment="Center" Margin="10,26,0,0" Grid.Row="2" VerticalAlignment="Top" Height="26" Width="181"/>
<Label x:Name="lblLogged" Content="Logged Out" HorizontalContentAlignment="Center" Margin="10,0" Grid.Row="2" VerticalAlignment="Top" Width="181"/>
<ContentControl Grid.Row="1" Grid.Column="1"
x:Name="ActiveItem" Margin="0,0,-647,0"
Background="Blue"/>
</Grid>
</Window>
Code for ShellViewModel:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using Caliburn.Micro;
using ProductivityTool.Models;
using ProductivityTool.Views;
namespace ProductivityTool.ViewModels
{
public class ShellViewModel : Conductor<object>
{
//private string _lblLogged;
//private string lblLoggedInAs;
private string _firstName = "Tim"; // Don't change this
private string _lastName;
private BindableCollection<PersonModel> _people = new BindableCollection<PersonModel>();
private PersonModel _selectedPerson;
public ShellViewModel() //Constructor
{
People.Add(new PersonModel { FirstName = "Tim", LastName = "Corey" });
People.Add(new PersonModel { FirstName = "Bill", LastName = "Jones" });
People.Add(new PersonModel { FirstName = "Sam", LastName = "Yet" });
}
public string FirstName
{
get { return _firstName; }
set
{
_firstName = value;
NotifyOfPropertyChange(() => FirstName);
NotifyOfPropertyChange(() => FullName); //Whenever a value of first name is changed, update fullname
}
}
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
NotifyOfPropertyChange(() => LastName);
NotifyOfPropertyChange(() => FullName); //Whenever a value of last name is changed, update fullname
}
}
public String FullName
{
get { return $"{ FirstName } { LastName }"; }
}
public BindableCollection<PersonModel> People
{
get { return _people; }
set { _people = value; }
}
public PersonModel SelectedPerson
{
get { return _selectedPerson; }
set
{
_selectedPerson = value;
NotifyOfPropertyChange(() => SelectedPerson);
}
}
//Return true or true for yes we can clear the text
public bool CanClearText(string firstName, string lastName)
{
//return !String.IsNullOrWhiteSpace(firstName) || !String.IsNullOrWhiteSpace(lastName);
if (String.IsNullOrWhiteSpace(firstName) && String.IsNullOrWhiteSpace(lastName))
{
return false;
}
else
{
return true;
}
}
//Perameters should start with lowercase, properties should start with uppercase
public void ClearText(string firstName, string lastName)
{
FirstName = "";
LastName = "";
}
public void LoadWelcomePage()
{
ActivateItem(new WelcomeViewModel());
}
public void LoadPageTwo()
{
//ActivateItem(new RegistrationViewModel());
}
}
}
Code-behind for ShellView (database info removed for privacy):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Data;
using System.Data.SqlClient;
using System.Text.RegularExpressions;
using ProductivityTool.ViewModels;
namespace ProductivityTool.Views
{
/// <summary>
/// Interaction logic for ShellView.xaml
/// </summary>
public partial class ShellView : Window
{
private ShellViewModel _viewModel; //this is needed to access methods from the view model, format _viewModel.method();
public ShellView()
{
InitializeComponent();
_viewModel = new ShellViewModel();
this.DataContext = _viewModel;
}
private void LoginBtn_Click(object sender, RoutedEventArgs e)
{
string loginCredentials;
if (string.IsNullOrWhiteSpace(textBoxEmail.Text))
{
loginCredentials = textBoxUID.Text;
}
else
{
loginCredentials = textBoxEmail.Text;
}
if (string.IsNullOrEmpty(loginCredentials))
{
MessageBox.Show("No credentials entered.");
}
else
{
string email = loginCredentials;
SqlConnection con = new SqlConnection("Data Source=source;Database=database;Trusted_Connection=Yes;");
con.Open();
SqlCommand cmd = new SqlCommand("Select * from UProfile where Email='" + email + "'", con);
cmd.CommandType = CommandType.Text;
SqlDataAdapter adapter = new SqlDataAdapter();
adapter.SelectCommand = cmd;
DataSet dataSet = new DataSet();
adapter.Fill(dataSet);
if (dataSet.Tables[0].Rows.Count > 0)
{
string username = dataSet.Tables[0].Rows[0]["UserName"].ToString();
}
else
{
MessageBox.Show("Error");
}
lblLogged.Content = "Signed in as:";
lblSignedInAs.Content = dataSet.Tables[0].Rows[0]["UserName"].ToString(); //Display who is signed in currently
con.Close();
_viewModel.LoadWelcomePage();
}
}
private void Phonebook_Click(object sender, RoutedEventArgs e)
{
_viewModel.LoadWelcomePage();
}
}
}
Here is the WelcomeViewModel class that I want to call:
using Caliburn.Micro;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ProductivityTool.Models;
namespace ProductivityTool.ViewModels
{
public class WelcomeViewModel : Screen
{
}
}
This is identical to the FirstChildViewModel that the tutorial was based on that was working at one point. Here is that:
using Caliburn.Micro;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ProductivityTool.ViewModels
{
public class FirstChildViewModel : Screen
{
}
}
Apologies for messy code, I want to include everything so you guys have the full picture. Most of it is legacy from the tutorials I did that I keep for reference. If there is a better way to achieve this effect, please let me know.
I stepped through the code and verified that LoadWelcomePage is triggering. I also tried setting this method to be triggered from the button on the menu to the left and it didn't change anything. I messed with the order of ContentControl in the xaml thinking it's a z-index thing, but that didn't work. Actually assigning Z-index values also didn't work. I removed the login border and all of its children from the screen and just tried to have the ContentControl appear upfront but the Z-index looks proper it doesn't work correctly. I also tried different window and grid backgrounds, like transparent, thinking it was hidden behind either the grid or the window but it always remains hidden. I also tried setting the d:DataContext as recommended by another post but it didn't change anything.
While doing the tutorials, I had the same buttons and views being triggered and it was working, but now that my use-case is slightly different things have broken. I'm mostly convinced it is a z-index issue, but it's a simple layout and things seem to be as they should so I am stuck. Thanks in advance for the help.
After more testing, I find out that my _viewModel.LoadWelcomePage is triggering, but not actually doing anything. Assigning the methods that activateitem to different buttons and clicking those makes everything work fine. Because this is more of a c# question, I will close this question and open another one if I can't figure out how to get my method trigger to work.
Here is what my Datagrid looks like, I'm using a DataContext and a CollectionViewSource in order to populate the Grid.
Here are the sample codes that I did in order to do this:
<CollectionViewSource x:Key="BeginningBalanceViewSource" d:DesignSource="{d:DesignInstance {x:Type reportModel:BeginningBalance}, CreateList=True}"/>
<DataGrid DataContext="{Binding Source={StaticResource BeginningBalanceViewSource}}" ItemsSource="{Binding Source={StaticResource BeginningBalanceViewSource}}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Denominations" MinWidth="130">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox BorderThickness="0" IsReadOnly="True" Text="{Binding BBDenomination, Mode=TwoWay, UpdateSourceTrigger=Explicit}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Pieces">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<controls:NumericUpDown BorderThickness="0" Minimum="0" Value="{Binding BBPieces, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Amount" MinWidth="130">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox BorderThickness="0" IsReadOnly="True" Text="{Binding BBAmount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Here is how I change my CollectionViewSource to update the DataContext (which I got from MSDN):
private CollectionViewSource _beginningBalanceViewSource;
private BeginningBalance _beginningBalance;
_beginningBalanceViewSource = ((CollectionViewSource)(FindResource("BeginningBalanceViewSource")))
_beginningBalance.Source = _entities.BeginningBalances.ToList().Where(x=>x.BBDateManaged.Equals(d)).ToList();
I edited the entity type BeginningBalance.cs (which is the ObservableCollection)
public partial class BeginningBalance : INotifyPropertyChanged
{
public int BBID { get; set; }
public double BBDenomination { get; set; }
private int pieces;
public int BBPieces { get { return pieces; }
set
{
pieces = value;
OnPropertyChanged("BBPieces");
OnPropertyChanged("BBAmount");
} }
public double BBAmount { get; set;}
//public double BBAmount { get{ return BBDenomination * BBPieces; } }
//sadly, it returns an error stating that The entity type BeginningBalance is not part of the model for the current context if I ever were to do this.
public System.DateTime BBDateManaged { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
If you noticed the code, I added INotifyPropertyChanged in order to test things out a bit and see if the Amount (Third Column of the DataGrid) would change (I know what I'm doing is probably wrong please forgive my stupidity as I lack experience in WPF)
Everything is working fine, the only issue I'm facing is that I only need to know how to get the value of the first 2 cells of the Datagrid then change the value of the third cell through binding. The first 2 cell will be multiplied to each other then the third cell would get the multiplied value automatically once I changed the 2nd cell's value. The NumericUpDownColumn is the only non read only and I tried adding an event and yes, it somehow works (but it kinda messes up my way of coding).
Do I also have to create a ViewModelClass for the Beginning Balance? If I have to, I dont know of a way on how to Save Changes on button click which mine is:
_entities.SaveChanges();
which will directly save to the database.
I'll look into the MVVM Pattern after I solve this issue.
Ok, sorry for being an idiot, it was just as simple as just changing my
public double BBAmount { get; set;}
to
public double BBAmount { get{ return BBDenomination * BBPieces; }
set { }
}
I just forgot to add a set{} damn.
Is it the intended behavior that a binding to a collection automatically uses the first item as source?
Example Xaml:
<Window x:Class="ListSelection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBlock Text="{Binding ColContent}" />
<TextBlock Text="{Binding ItemContent}" />
</StackPanel>
</Window>
and Code:
using System.Collections.Generic;
using System.Windows;
namespace ListSelection
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MyCol("col 1")
{
new MyItem("item 1"),
new MyItem("item 2")
};
}
}
public class MyItem
{
public string ItemContent { get; set; }
public MyItem(string content)
{
ItemContent = content;
}
}
public class MyCol : List<MyItem>
{
public string ColContent { get; set; }
public MyCol(string content)
{
ColContent = content;
}
}
}
The UI shows up with:
col 1
item 1
The second binding took implicitly the first collection item as source! So bug, feature or intended?
EDIT: .net 4.5, VS2012, corrections
EDIT 2:
I further investigated the problem together with a mate and got closer to the solution:
<Window x:Class="ListSelection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<ListView ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ItemContent}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBlock Text="{Binding ItemContent}" />
</StackPanel>
</Window>
The - lets call it - magic binding seems to exist for master detail views. By default any collection that is bound gets a CollectionView - which provides a selected item property (and other cool stuff like sorting). This selected item can be used shortcutted for the detailed view. If the IsSynchronizedWithCurrentItem is set to true the shortcutted binding reacts to changed selections. The problem in the whole thing: the selected item of the CollectionView is alway set to the first item which leads to the magic binding... I would call that a bug and it should only work explicitly, e.g. by binding the collection to a Selector with the IsSynchronizedWithCurrentItem set.
I have a Silverlight combo box outside of a grid which works fine.
However, I cannot get it to work properly inside the data grid. I am not certain what I am doing incorrectly. Help with this is greatly appreciated!
This code works fine for the silverlight combo box outside of the grid:
XAML:
<ComboBox Height="23" HorizontalAlignment="Left" ItemsSource="{Binding ElementName=comboBoxItemDomainDataSource, Path=Data}" Margin="112,72,0,0" Name="comboBoxItemComboBox" VerticalAlignment="Top" Width="185" SelectionChanged="comboBoxItemComboBox_SelectionChanged" DisplayMemberPath="ComboDisplayValue">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
</Grid>
<riaControls:DomainDataSource AutoLoad="True" d:DesignData="{d:DesignInstance my:ComboBoxItem, CreateList=true}" Height="0" LoadedData="comboBoxItemDomainDataSource_LoadedData" Name="comboBoxItemDomainDataSource" QueryName="GetComboboxItems_PatIdEssentrisQuery" Width="0">
<riaControls:DomainDataSource.DomainContext>
<my:ComboBoxItemContext />
</riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>
Combo Box Class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
namespace CorporateHR.Web
{
public class ComboBoxItem
{
[Key]
public int ComboID_Int { get; set; }
public string ComboDisplayValue { get; set; }
private static List<ComboBoxItem> GetComboBoxItems(string strStoredProcedure)
{
SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["RefConnectionString"].ConnectionString);
SqlCommand cmd = new SqlCommand(strStoredProcedure, con);
cmd.CommandType = System.Data.CommandType.StoredProcedure;
List<ComboBoxItem> comboList = new List<ComboBoxItem>();
con.Open();
SqlDataReader dr = cmd.ExecuteReader(behavior: CommandBehavior.CloseConnection);
while (dr.Read())
{
ComboBoxItem ComboBoxItem = new ComboBoxItem();
ComboBoxItem.ComboID_Int = Convert.ToInt32(dr[0].ToString());
ComboBoxItem.ComboDisplayValue = dr[1].ToString();
comboList.Add(ComboBoxItem);
}
return comboList;
}
public static List<ComboBoxItem> GetComboboxItems_PatIdEssentris()
{
return GetComboBoxItems("uspLookupPatIdEssentris");
}
//Secondary ComboBox Lookup:
public static List<ComboBoxItem> GetComboboxItems_ORStatus()
{
return GetComboBoxItems("uspLookupORStatus");
}
}
}
Combo Box Domain Service:
namespace CorporateHR.Web
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
// TODO: Create methods containing your application logic.
[EnableClientAccess()]
public class ComboBoxItemService : DomainService
{
public IEnumerable<ComboBoxItem> GetComboboxItems_PatIdEssentris()
{
return ComboBoxItem.GetComboboxItems_PatIdEssentris();
}
public IEnumerable<ComboBoxItem> GetComboboxItems_ORStatus()
{
return ComboBoxItem.GetComboboxItems_ORStatus();
}
}
}
Code Behind Page (for combo box which populates):
private void comboBoxItemDomainDataSource_LoadedData(object sender, LoadedDataEventArgs e)
{
if (e.HasError)
{
System.Windows.MessageBox.Show(e.Error.ToString(), "Load Error", System.Windows.MessageBoxButton.OK);
e.MarkErrorAsHandled();
}
}
private void comboBoxItemComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
}
I used a templated column like:
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Margin="2" VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{Binding Path=Option0, Mode=OneWay}" Width="Auto" />
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>
<sdk:DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<ComboBox Height="23" Name="cbx0" SelectedValuePath="Display" DisplayMemberPath="Display"
SelectedValue="{Binding Path=Option0, Mode=TwoWay}"
ItemsSource="{Binding Source={StaticResource DataContextProxy},Path=DataSource.ocList0}"
MinWidth="65"
Width="Auto">
</ComboBox>
</StackPanel>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellEditingTemplate>
Where _ocList0 is of type ObservableCollection<cComboBoxOption>
And this is the cComboBoxOption class:
public class cComboBoxOption
{
public int Id { get; set; }
public string Display { get; set; }
public cComboBoxOption(int id, string name)
{
this.Id = id;
this.Display = name;
}
}
This was written in a generic way because I didn't know what the bindings would be or what the combo box would contain until run time.
A simpler way to do this is to use List<string> see the blog post HERE.
I'm creating a WPF application using the MVVM design pattern that consists of a ListView and some ComboBoxes. The ComboBoxes are used to filter the ListView. What I am trying to accomplish is populating the combobox with items in the related ListView column. In other words, if my ListView has Column1, Column2, and Column3, I want ComboBox1 to display all UNIQUE items in Column1. Once an item is selected in the ComboBox1, I want the items in ComboBox2 and ComboBox3 to be filtered based on ComboBox1's selection, meaning that ComboBox2 and ComboBox3 can only contain valid selections. This would be somewhat similar to a CascadingDropDown control if using the AJAX toolkit in ASP.NET, except the user can select any ComboBox at random, not in order.
My first thought was to bind ComboBoxes to the same ListCollectionView that the ListView is bound to, and set the DisplayMemberPath to the appropriate column. This works great as far as filtering the ListView and ComboBoxes together goes, but it displays all items in the ComboBox rather than just the unique ones (obviously). So my next thought was to use a ValueConverter to only return the only the unique items, but I have not been sucessful.
FYI: I read Colin Eberhardt's post on adding a AutoFilter to a ListView on CodeProject, but his method loops through each item in the entire ListView and adds the unique ones to a collection. Although this method works, it seems that it would be very slow for large lists.
Any suggestions on how to achieve this elegantly? Thanks!
Code Example:
<ListView ItemsSource="{Binding Products}" SelectedItem="{Binding SelectedProduct}">
<ListView.View>
<GridView>
<GridViewColumn Header="Item" Width="100" DisplayMemberBinding="{Binding ProductName}"/>
<GridViewColumn Header="Type" Width="100" DisplayMemberBinding="{Binding ProductType}"/>
<GridViewColumn Header="Category" Width="100" DisplayMemberBinding="{Binding Category}"/>
</GridView>
</ListView.View>
</ListView>
<StackPanel Grid.Row="1">
<ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"/>
<ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="ProductType"/>
<ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="Category"/>
</StackPanel>
Check this out:
<Window x:Class="DistinctListCollectionView.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DistinctListCollectionView"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<local:PersonCollection x:Key="data">
<local:Person FirstName="aaa" LastName="xxx" Age="1"/>
<local:Person FirstName="aaa" LastName="yyy" Age="2"/>
<local:Person FirstName="aaa" LastName="zzz" Age="1"/>
<local:Person FirstName="bbb" LastName="xxx" Age="2"/>
<local:Person FirstName="bbb" LastName="yyy" Age="1"/>
<local:Person FirstName="bbb" LastName="kkk" Age="2"/>
<local:Person FirstName="ccc" LastName="xxx" Age="1"/>
<local:Person FirstName="ccc" LastName="yyy" Age="2"/>
<local:Person FirstName="ccc" LastName="lll" Age="1"/>
</local:PersonCollection>
<local:PersonAutoFilterCollection x:Key="data2" SourceCollection="{StaticResource data}"/>
<DataTemplate DataType="{x:Type local:Person}">
<WrapPanel>
<TextBlock Text="{Binding FirstName}" Margin="5"/>
<TextBlock Text="{Binding LastName}" Margin="5"/>
<TextBlock Text="{Binding Age}" Margin="5"/>
</WrapPanel>
</DataTemplate>
</Window.Resources>
<DockPanel>
<WrapPanel DockPanel.Dock="Top">
<ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[0]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
<ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[1]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
<ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[2]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
</WrapPanel>
<ListBox ItemsSource="{Binding Source={StaticResource data2}, Path=FilteredCollection}"/>
</DockPanel>
</Window>
And the view model:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.ComponentModel;
namespace DistinctListCollectionView
{
class AutoFilterCollection<T> : INotifyPropertyChanged
{
List<AutoFilterColumn<T>> filters = new List<AutoFilterColumn<T>>();
public List<AutoFilterColumn<T>> Filters { get { return filters; } }
IEnumerable<T> sourceCollection;
public IEnumerable<T> SourceCollection
{
get { return sourceCollection; }
set
{
if (sourceCollection != value)
{
sourceCollection = value;
CalculateFilters();
}
}
}
void CalculateFilters()
{
var propDescriptors = typeof(T).GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
foreach (var p in propDescriptors)
{
Filters.Add(new AutoFilterColumn<T>()
{
Parent = this,
Name = p.Name,
Value = null
});
}
}
public IEnumerable GetValuesForFilter(string name)
{
IEnumerable<T> result = SourceCollection;
foreach (var flt in Filters)
{
if (flt.Name == name) continue;
if (flt.Value == null || flt.Value.Equals("All")) continue;
var pdd = typeof(T).GetProperty(flt.Name);
{
var pd = pdd;
var fltt = flt;
result = result.Where(x => pd.GetValue(x, null).Equals(fltt.Value));
}
}
var pdx = typeof(T).GetProperty(name);
return result.Select(x => pdx.GetValue(x, null)).Concat(new List<object>() { "All" }).Distinct();
}
public AutoFilterColumn<T> GetFilter(string name)
{
return Filters.SingleOrDefault(x => x.Name == name);
}
public IEnumerable<T> FilteredCollection
{
get
{
IEnumerable<T> result = SourceCollection;
foreach (var flt in Filters)
{
if (flt.Value == null || flt.Value.Equals("All")) continue;
var pd = typeof(T).GetProperty(flt.Name);
{
var pdd = pd;
var fltt = flt;
result = result.Where(x => pdd.GetValue(x, null).Equals(fltt.Value));
}
}
return result;
}
}
internal void NotifyAll()
{
foreach (var flt in Filters)
flt.Notify();
OnPropertyChanged("FilteredCollection");
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
#endregion
}
class AutoFilterColumn<T> : INotifyPropertyChanged
{
public AutoFilterCollection<T> Parent { get; set; }
public string Name { get; set; }
object theValue = null;
public object Value
{
get { return theValue; }
set
{
if (theValue != value)
{
theValue = value;
Parent.NotifyAll();
}
}
}
public IEnumerable DistinctValues
{
get
{
var rc = Parent.GetValuesForFilter(Name);
return rc;
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
#endregion
internal void Notify()
{
OnPropertyChanged("DistinctValues");
}
}
}
The other classes:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DistinctListCollectionView
{
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DistinctListCollectionView
{
class PersonCollection : List<Person>
{
}
class PersonAutoFilterCollection : AutoFilterCollection<Person>
{
}
}
If you are using MVVM, then all of your bound data objects are in your ViewModel class, and your ViewModel class is implementing INotifyPropertyChanged, correct?
If so, then you can maintain state variables for SelectedItemType1, SelectedItemType2, etc., that are bound to your ComboBox(es) SelectedItem dependency property. In the Setter for SelectedItemType1, populate the List property (which is bound to the ItemsSource for ComboBoxType2) and fire the NotifyPropertyChanged for the List property. Repeat this for Type3 and you should be in the ballpark.
As for the "refresh" issue, or how does the View know when something has changed, it all comes down to the binding mode and firing the NotifyPropertyChanged event at the correct moments.
You could do this with a ValueConverter, and I love ValueConverters, but I think in this case it is more elegant to manage your ViewModel so that the Binding just happens.
Why not just create another property that contained only the distinct values from the list using a linq query or something like that?
public IEnumerable<string> ProductNameFilters
{
get { return Products.Select(product => product.ProductName).Distinct(); }
}
...etc.
You'll have to raise property changed notifications for each of the filter lists when your Product property changes, but that's not a big deal.
You should really consider your ViewModel as a big ValueConverter for your view. The only time I would use a ValueConverter in MVVM is when I need to change data from a data type that is not view-specific to one that is view-specific. Example: for values greater than 10, text needs to be Red and for values Less than 10, text needs to be Blue... Blue and Red are view-specific types and shouldn't be something that gets returned from a ViewModel. This is really the only case where this logic shouldn't be in a ViewModel.
I question the validity of the "very slow for large lists" comment... generally the "large" for humans and "large" for a computer are two very different things. If you are in the realm of "large" for both computers and humans, I would also question showing this much data on a screen. Point being, it's likely not large enough for you to notice the cost of these queries.