I am using VB/wpf and SQL Server. I am NOT using mvvm, a datatable, a button, a dataset or a windows form. Under those conditions and a couple of days of searching, I cannot find the final step to saving the data from a grid.
My database table tracks donations; it has the usual, expected fields. Here is what I have so far:
XAML (snippet):
<Window.Resources>
<CollectionViewSource
Filter="Filter_By_Member"
x:Key="cvsDonations">
</CollectionViewSource>
.....
<DataGrid
AutoGenerateColumns="False"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
CanUserResizeRows="False"
CanUserDeleteRows="True"
FontSize="13"
FontWeight="Normal"
ItemsSource="{Binding Source={StaticResource cvsDonations}}"
Name="dgDonations"
RowHeaderWidth="20"
SelectionUnit="CellOrRowHeader"
SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn
Binding="{Binding Path=DateDue, Converter={StaticResource conDate}, StringFormat='MMM d, yyyy'}"
ElementStyle="{StaticResource styDateBlock}"
Header="Date Due"
IsReadOnly="True"
Width="90">
</DataGridTextColumn>
.....
My window contains a ComboBox and the grid. The rows in the grid depend on what is selected in the ComboBox. Here is the code that does that.
Private Sub Change_Member(sender As Object, e As SelectionChangedEventArgs) Handles cboMembers.SelectionChanged
cvsDonations.View.Refresh()
End Sub
Private Sub Filter_By_Member(sender As Object, e As FilterEventArgs)
Dim PersonID As Long = CLng(cboMembers.SelectedValue)
Dim d As Donation = DirectCast(e.Item, Donation)
If d.PersonID = PersonID Then
e.Accepted = True
Else
e.Accepted = False
End If
End Sub
So far everything works. What I haven't figured out is how to automatically update the underlying database (using stored procedures which are already written and working). More specifically, I cannot figure out what event to watch for and how to implement that event in code.
The underlying collection is an ObservableCollection(Of Donation) and the Donation class has implemented INotifyPropertyChanged. I've considered watching the property changed but that event gets fired every time the grid is populated so that doesn't seem like the right approach, besides I can't figure out how to actually raise the appropriate event even if that is the way to go.
I've tried using the RowEditChanging event which seems to be ALMOST what I want but it is fired before any edits are submitted so the donation the row contains has the old value when this event is fired.
What is the right approach and how should it be implemented given my overall set up?
I suggest doing it this way -
Your Donation class should have a flag field, let's say Is_Dirty which keeps track of whether the object has modifications or not.
When you initially load items in your collection, make sure Is_Dirty is set to false. When user makes edit on grid and property notification is fired, set the dirty flag (from property setter).
For saving, I suggest having a separate button, clicking on which you go through all dirty objects from your collection and pass updates to database.
By binding to dirty flag, you can also change the appearance of modified rows for user to identify modified rows.
You can also maintain a State property, to know whether the object is Added, Updated or Deleted.
Typically, I put all this functionality in a base class from which all my model classes (e.g. Donation) derives.
Some snippet to get you going...
public abstract class ObservableDomainObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool _isDirty;
public ObservableDomainObject()
{
_state = ObjectState.Added;
_isDirty = true;
}
private ObjectState _state;
/// <summary>
/// Get or Set
/// </summary>
public ObjectState State
{
get { return _state; }
set
{
if (_state != value)
{
_state = value;
RaisePropertyChanged();
}
}
}
public void SetDirty(Boolean isDirty)
{
this.IsDirty = isDirty;
}
/// <summary>
/// Check if the object is modified.
/// </summary>
public bool IsDirty
{
get { return _isDirty; }
set
{
if (_isDirty != value)
{
if (_state != ObjectState.Added && _state != ObjectState.Deleted)
{
this.State = value ? ObjectState.Modified : ObjectState.Unchanged;
}
_isDirty = value;
RaisePropertyChanged();
}
}
}
protected virtual void RaisePropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
// Change Dirty flag when property changed.
if (propertyName != "IsDirty")
SetDirty(true);
}
}
public enum ObjectState
{
/// <summary>
/// New Record
/// </summary>
Added = 0,
/// <summary>
/// Unchanged after the load
/// </summary>
Unchanged,
/// <summary>
/// Modified
/// </summary>
Modified,
/// <summary>
/// Deleted
/// </summary>
Deleted
}
Related
I'm filling the itemssource of a WPF combobox in code-behind with a datatable containing the columns "Listkey" and "Listvalue" like that:
SetElementProperty(element, "ItemsSource", (new ListtablesRead()).ReadListtable(changeTextProperties.SelectedListTable).DefaultView);
SetElementProperty(element, "DisplayMemberPath", "Listvalue");
SetElementProperty(element, "SelectedValuePath", "Listkey");
SetElementProperty is a method which checks by reflection, if the frameworkelement (in that case the combobox) has the given property and sets it.
Then i want to serialize the control with XmlWriter.
So i wrote a converter class for the type DataRowView:
using System;
using System.ComponentModel;
using System.Data;
using System.Windows;
using System.Windows.Markup;
namespace WPFDesignerConverterLibrary
{
public class DataRowViewConverter : ExpressionConverter
{
/// <summary>
/// Finds out if the converter can convert an expression-object to the given destinationtype.
/// </summary>
/// <param name="context">An ITypeDescriptorContext-interface which provides a context for formatting.</param>
/// <param name="destinationType">A type-class which represents the target-type of the conversion.</param>
/// <returns>Returns an object of type bool. True = the destinationtype can be converted.</returns>
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(MarkupExtension))
return true;
return false;
}
/// <summary>
/// Converts the expression to the given destinationtype.
/// </summary>
/// <param name="context">An ITypeDescriptorContext-interface which provides a context for formatting.</param>
/// <param name="culture">The System.Globalization.CultureInfo which is actually used as culture.</param>
/// <param name="value">The object to convert.</param>
/// <param name="destinationType">A type-class which represents the target-type of the conversion.</param>
/// <returns></returns>
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value,
Type destinationType)
{
if (destinationType == typeof(MarkupExtension))
{
DataRowView datarowview = value as DataRowView;
if (datarowview == null)
throw new Exception();
return datarowview.Row;
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
}
This converter works and produces the following lines in the serialized XML:
<sd:DataRow RowError="">
<sd:DataRow.ItemArray>
<x:Array Type="s:Object" xml:space="preserve"><s:String>01</s:String><s:String>Ersttest </s:String></x:Array>
</sd:DataRow.ItemArray>
</sd:DataRow>
<sd:DataRow RowError="">
<sd:DataRow.ItemArray>
<x:Array Type="s:Object" xml:space="preserve"><s:String>02</s:String><s:String>Wiederholungstest </s:String></x:Array>
</sd:DataRow.ItemArray>
</sd:DataRow>
<sd:DataRow RowError="">
<sd:DataRow.ItemArray>
<x:Array Type="s:Object" xml:space="preserve"><s:String>03</s:String><s:String>Konstanzprüfung </s:String></x:Array>
</sd:DataRow.ItemArray>
</sd:DataRow>
But when i try to reload the serialized XML i get an error message, which says, that no standard constructor for the type DataRow was found.
What's going wrong?
And further: To set the itemssource of the combobox with a datatable's defaultview is the simplest way to do this, but can it be that i have to go another way?
InnerException:
=-2146233069 HResult
Message=For the type "System. Data. DataRow" no standard constructor was found. The type can be provided with the argument or the FactoryMethod directive.
Source=System. Xaml
StackTrace:
at system. Xaml. Pattern. XamlTypeInvoker. DefaultCtorXamlActivator. EnsureConstructorDelegate (XamlTypeInvoker type)
at system. Xaml. Pattern. XamlTypeInvoker. CreateInstance (Object [] of argument)
at MS.Internal. Xaml. Run time. ClrObjectRuntime. CreateInstanceWithCtor (XamlType xamlType, Object [] args)
at MS.Internal. Xaml. Run time. ClrObjectRuntime. CreateInstance (XamlType xamlType, Object [] args)
Meanwhile i have found another way.
I'm using a dummy-class called KeyAndValue:
/// <summary>
/// Dummy class with key and value properties for construction of comboboxitems.
/// </summary>
public class KeyAndValue
{
/// <summary>
/// Gets or sets the key.
/// </summary>
public string Key { get; set; }
/// <summary>
/// Gets or sets the value.
/// </summary>
public string Value { get; set; }
}
That class helps to fill the items-collection of the combobox:
foreach (System.Data.DataRow row in table.Rows)
{
// Add new pairs with Listkey and Listvalue as content to the Items-collection.
customComboBox.Items.Add(new KeyAndValue() { Key = row["Listkey"].ToString(), Value = row["Listvalue"].ToString() });
}
That's a little bit like using KeyValuePair with one diffenrence: A generic list like KeyValuePair cannot be serialized.
But my dummy-class does it.
What you need is to have a class with parameterless contrsuctor aka Default Constructor that will allow Serialization.
Step 1.
Create a class to hold your database results.
You can do this through manual code or you could use DataSet.
Step 2.
After you created the class or used the dataset serialize it.
FileStream fs = new FileStream(savePath + "\\" + a.Location + ".bin", FileMode.Create);
BinaryFormatter serializer = new BinaryFormatter();
try
{
serializer.Serialize(fs, a);
}
catch (Exception e)
{
//handle your fail;
}
finally
{
fs.Close();
}
or deserialize it:
List<Model.YOURCLASS> _l = new List<Model.YOURCLASS>();
string[] files = Directory.GetFiles(pathToSearch);
FileStream fs;
BinaryFormatter deserializer = new BinaryFormatter();
foreach (var a in files)
{
if(Path.GetExtension(a) == ".bin")
{
fs = new FileStream(a, FileMode.Open);
_l.Add((Model.YOURCLASS)deserializer.Deserialize(fs));
}
}
return _l;
The type that your routine returns doesn't have to be a DataTable your ListView will automatically use any collection you pass it, even an array[]
Using VB.net & WPF
I've converted code available at Overlaying Controls in WPF with Adorners from C# to VB.Net
Original C# Code
/// <summary>
/// Overlays a control with the specified content
/// </summary>
/// <typeparam name="TOverlay">The type of content to create the overlay from</typeparam>
public class OverlayAdorner<TOverlay> : Adorner, IDisposable where TOverlay : UIElement, new()
{
private UIElement _adorningElement; private AdornerLayer _layer; /// <summary> /// Overlay the specified element /// </summary> /// <param name="elementToAdorn">The element to overlay</param> /// <returns></returns> public static IDisposable Overlay(UIElement elementToAdorn) { return Overlay(elementToAdorn, new TOverlay()); }
/// <summary>
/// Overlays the element with the specified instance of TOverlay
/// </summary>
/// <param name="elementToAdorn">Element to overlay</param>
/// <param name="adorningElement">The content of the overlay</param>
/// <returns></returns>
public static IDisposable Overlay(UIElement elementToAdorn, TOverlay adorningElement)
{
var adorner = new OverlayAdorner<TOverlay>(elementToAdorn, adorningElement);
adorner._layer = AdornerLayer.GetAdornerLayer(elementToAdorn);
adorner._layer.Add(adorner);
return adorner as IDisposable;
}
private OverlayAdorner(UIElement elementToAdorn, UIElement adorningElement)
: base(elementToAdorn)
{
this._adorningElement = adorningElement;
if (adorningElement != null)
{
AddVisualChild(adorningElement);
}
Focusable = true;
}
protected override int VisualChildrenCount
{
get { return _adorningElement == null ? 0 : 1; }
}
protected override Size ArrangeOverride(Size finalSize)
{
if (_adorningElement != null)
{
Point adorningPoint = new Point(0, 0);
_adorningElement.Arrange(new Rect(adorningPoint, this.AdornedElement.DesiredSize));
}
return finalSize;
}
protected override Visual GetVisualChild(int index)
{
if (index == 0 && _adorningElement != null)
{
return _adorningElement;
}
return base.GetVisualChild(index);
}
public void Dispose()
{
_layer.Remove(this);
}
}
VB.Net Code (Converted by Me)
Public Class OverlayAdorner(Of TOverlay As {UIElement, New})
Inherits Adorner
Implements IDisposable
Private _adorningElement As UIElement
Private _layer As AdornerLayer
Public Shared Function Overlay(elementToAdorn As UIElement, adorningElement As TOverlay) As IDisposable
Dim adorner = New OverlayAdorner(Of TOverlay)(elementToAdorn, adorningElement)
adorner._layer = AdornerLayer.GetAdornerLayer(elementToAdorn)
adorner._layer.Add(adorner)
Return TryCast(adorner, IDisposable)
End Function
Private Sub New(elementToAdorn As UIElement, adorningElement As UIElement)
MyBase.New(elementToAdorn)
Me._adorningElement = adorningElement
If adorningElement IsNot Nothing Then
AddVisualChild(adorningElement)
End If
Focusable = True
End Sub
Protected Overrides ReadOnly Property VisualChildrenCount() As Integer
Get
Return If(_adorningElement Is Nothing, 0, 1)
End Get
End Property
Protected Overrides Function ArrangeOverride(finalSize As Size) As Size
If _adorningElement IsNot Nothing Then
Dim adorningPoint As New Point(0, 0)
_adorningElement.Arrange(New Rect(adorningPoint, Me.AdornedElement.DesiredSize))
End If
Return finalSize
End Function
Protected Overrides Function GetVisualChild(index As Integer) As Visual
If index = 0 AndAlso _adorningElement IsNot Nothing Then
Return _adorningElement
End If
Return MyBase.GetVisualChild(index)
End Function
Public Sub Dispose() Implements IDisposable.Dispose
_layer.Remove(Me)
End Sub
End Class
Now I've created MainWindow & an User Control UserControl1 in my test project and trying code
Using OverlayAdorner(Of UserControl1).Overlay(G1)
'Err in First Line Itself
End Using
Error Argument not specified for parameter 'adorningElement' of 'Public Shared Function Overlay(elementToAdorn As System.Windows.UIElement, adorningElement As TOverlay) As System.IDisposable'.
What is Wrong Here
Below code block given on blog post you are referring to has incorrect usage shown.
using (OverlayAdorner<ProgressMessage>.Overlay(LayoutRoot))
{
// Do some stuff here while adorner is overlaid
}
Should be (in C#):
using (OverlayAdorner<ProgressMessage>.Overlay(LayoutRoot, this)) // Here I assume `this` is somehow `UserControl`'s object
{
// Do some stuff here while adorner is overlaid
}
In VB.NET
Using OverlayAdorner(Of UserControl).Overlay(G1, UserControl1)
' Do some stuff here while adorner is overlaid
End Using
Also note that: OverlayAdorner(Of UserControl1) should be OverlayAdorner(Of UserControl) because T here should be Type's name not name of object instance.
I hope that helps.
Some preaching
From this question of yours I get hint that you need to work yourself on language more. Give more time to VB.NET or C#.NET and use intellisense in Visual Studio to get yourself familiar with the class library and parameter types to be passed into methods you use. This will help you working in new class libraries which have some to no documentation.
I use this shortcut more when working in new class libaries. Type ctrl+k then ctrl+i. Generally said as ctrl+k,i
Update
Based on recent comment, Check the actual usage below:
XAML snippet:
<Window x:Class="WpfTestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid x:Name="G1">
<UserControl x:Name="ucMyControl"></UserControl>
</Grid>
</Window>
Code behind snippet:
Using OverlayAdorner(Of UserControl).Overlay(G1, ucMyControl)
' Do some stuff here while adorner is overlaid
End Using
I have a DataGrid showing some databases having quite some columns.
I would like that, when the user edit a new row, some values are set automatically.
With the windows form DataGrid that would be easy, since there's RowsAdded event handler.
But how could i handle this with the wpf DataGrid ??
Edit : my DataGrid is bound in Xaml to a public property which is an ITable. When user select a table in a ComboBox, the property is updated with corresponding table.
Yes there's autogenerating column, and the way the user can enter a new row is to edit the last blank row (default behaviour).
You can do this in the LoadingRow event. Try something like this:
private void myDataGrid_LoadingRow(object sender, System.Windows.Controls.DataGridRowEventArgs e)
{
MyObject myObject = e.Row.Item as MyObject;
if (myObject != null)
{
myObject.PropertyOne = "test";
myObject.PropertyTwo = 2;
}
}
Ok i think i got it.
When a DataTable is bound to a DataGrid, a CollectionView is created in order to see it. You can get it by using the (static/shared) CollectionView.GetDefaultView(ThePropertyThatIsBound) method.
Since it implements ICollectionChanged, you can add an event handler to the CollectionChangedEvent.
In the CollectionChanged event handler, if you have a new item (e.NewItems.Count>0) you must check it against System.Windows.Data.CollectionView.NewItemPlaceholder and if it is not a place holder, then it is a brand new item, for wich i can set all default values.
Assign a CollectionViewSource to your DataGrid then listen to the CollectionChanged event as following :
..
public CollectionViewSource ViewSource { get; set; }
..
this.ViewSource = new CollectionViewSource();
this.ViewSource.Source = new List<YourObjectType>();
this.ViewSource.View.CollectionChanged += View_CollectionChanged;
..
private void View_CollectionChanged(object sender,System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems.Count > 0)
{
YourObjectType myObject = e.NewItems[e.NewItems.Count-1] as YourObjectType;
if (myObject != null)
{
myObject.Property = TheValueYouWant;
..
}
}
}
..
<DataGrid ItemsSource="{Binding ViewSource.View}" ../>
I have 2 windows w1 and w2 with one textbox each and data in txtbox1 in w1 needs to be appearing in w2 txtbox .How to achieve this using dependency properties
I suggest you rather use a common DataContext for your two windows than binding them to each other. As an example, let's assume you have this class as DataModel:
public class MyDataModel : INotifyPropertyChanged
{
private string text;
public string Text {
get { return text; }
set {
text = value;
RaisePropertyChanged("Text");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName) {
var handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Then you can set it as DataContext on your windows:
MyDataModel model = new MyDataModel();
model.Text = "Hello World";
MyWindow a = new MyWindow();
MyOtherWindow b = new MyOtherWindow();
a.DataContext = model;
b.DataContext = model;
If you have done this, you can set the Text Property of the TextBoxes in every window to a Binding like this:
<TextBox Text="{Binding Text}"/>
Both Textboxes will now automatically update if you set model.Text to another value.
Is there a mother of the 2 forms? a form that creates both of them?
In this case what I normally do, since I'm implementing INotifyOnPropertyChanged anyway, is in the mother I subscribe to the w1's PropertyChanged event, get the value, and then place it in the property of w2.
I wouldnt really use Dependency Properties
I know its not pretty but I generally prefer it because I like to keep the 1 viewmodel to 1 view relationship. My boss thinks its easier to read and understand this way.
I have the following XAML markup:
<TextBox x:Name="MyTextBox" Text="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Name="MyComboBox" ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"
SelectedValue="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber}"
SelectedValuePath="ProductNumber" />
My View's DataContext is bound to a viewmodel containing a public property called SelectedCustomer. Customer objects contain a FavouriteProduct property of type Product and Product objects contain public properties ProductNumber and ProductName.
The behaviour I'm looking for is to have the SelectedItem of the ComboBox update the Text in the TextBox and vice versa. ComboBox to TextBox works just fine. Selecting any product in the ComboBox updates the TextBox with the product number of that product. However when I try to go the other way I get som strange behaviour. It only works for the items that come before the selected item. I will try to explain:
Consider the following list of products ([Product Number], [Product Name]):
Fanta
Pepsi
Coca Cola
Sprite
Water
Now lets say that the SelectedCustomer's favourite product is Coca Cola (must be a developer). So when the window opens the TextBox reads 3 and the ComboBox reads Coca Cola. Lovely. Now lets change the product number in the TextBox to 2. The ComboBox updates it's value to Pepsi. Now try to change the product number in the TextBox to anything higher then the number for Coca Cola (3). Not so lovely. Selecting either 4 (Sprite) or 5 (Water) makes the ComboBox revert back to Coca Cola. So the behaviour seems to be that anything below the item that you open the window width from the list in the ItemSource does not work. Set it to 1 (Fanta) and none of the others work. Set it to 5 (Water) and they all work. Could this have to do with some initialisation for the ComboBox? Potential bug? Curious if anyone else have seen this behaviour.
UPDATE:
After reading Mike Brown's response I have created properties for SelectedProduct and SelectedProductNumber. The problem I am having with this is that as soon as you select something from the ComboBox you end up in an endless loop where the properties keep updatign each other. Have I implemented the OnPropertyChanged handler incorrectly or is there something I am missing? Here is a snippet of code from my ViewModel:
private int _SelectedProductNumber = -1;
public int SelectedProductNumber
{
get
{
if (_SelectedProductNumber == -1 && SelectedCustomer.Product != null)
_SelectedProductNumber = SelectedCustomer.Product.ProductNumber;
return _SelectedProductNumber;
}
set
{
_SelectedProductNumber = value;
OnPropertyChanged("SelectedProductNumber");
_SelectedProduct = ProductList.FirstOrDefault(s => s.ProductNumber == value);
}
}
private Product _SelectedProduct;
public Product SelectedProduct
{
get
{
if (_SelectedProduct == null)
_SelectedProduct = SelectedCustomer.Product;
return _SelectedProduct;
}
set
{
_SelectedProduct = value;
OnPropertyChanged("SelectedProduct");
_SelectedProductNumber = value.ProductNumber;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
UPDATE 2
I have changed the implementation slightly now by updating the SelectedCustomer.FavouriteProduct from both properties and then using that when reading their values. This now works but I'm not sure it's the 'correct way'.
private int _SelectedProductNumber = 0;
public int SelectedProductNumber
{
get
{
if (SelectedCustomer.Product != null)
_SelectedProductNumber = SelectedCustomer.Product.ProductNumber;
return _SelectedProductNumber;
}
set
{
_SelectedProductNumber = value;
SelectedCustomer.FavouriteProduct = ProductList.FirstOrDefault(s => s.ProductNumber == value);
OnPropertyChanged("SelectedProductNumber");
OnPropertyChanged("SelectedProduct");
}
}
private Product _SelectedProduct;
public Product SelectedProduct
{
get
{
if (SelectedCustomer.Product != null)
_SelectedProduct = SelectedCustomer.Product;
return _SelectedProduct;
}
set
{
_SelectedProduct = value;
SelectedCustomer.FavouriteProduct = value;
OnPropertyChanged("SelectedProduct");
OnPropertyChanged("SelectedProductNumber");
}
}
Your aim is not too clear so I have written the folloiwng so support either options I can see.
To keep two elements bound to one item in sync you can set the IsSynchronizedWithCurrentItem="True" on your combobox as shown below:
<TextBox x:Name="MyTextBox" Text="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Name="MyComboBox" ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"
SelectedValue="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber}"
IsSynchronizedWithCurrentItem="True"
SelectedValuePath="ProductNumber" />
This will mean everything in the current window bound to the same background object will keep in sync and not give the odd behaviours you are seeing.
This quote form this longer MSDN article describes the effect:
The IsSynchronizedWithCurrentItem
attribute is important in that, when
the selection changes, that is what
changes the "current item" as far as
the window is concerned. This tells
the WPF engine that this object is
going to be used to change the current
item. Without this attribute, the
current item in the DataContext won't
change, and therefore your text boxes
will assume that it is still on the
first item in the list.
Then setting the Mode=TwoWay as suggested by the other answer will only ensure that both when you update the textbox the underlying object will be updated and when you update the object the textbox is updated.
This makes the textbox edit the selected items text and not select the item in the combolist with the matching text (which is the alternative think you are may be trying to achieve?)
To achieve the synchronised selection effect it may be worth setting IsEditable="True" on the combobox to allow users to type items in and dropping the text box. Alternatively if you need two boxes replace the textbox with a second combobox with IsSynchronizedWithCurrentItem="True" and IsEditable="True" then a styled to make it like a text box.
What you want to do is expose separate properties on your ViewModel for the currently selected product and currently selected product number. When the selected product is changed, update the product number and vice versa. So your viewmodel should look something like this
public class MyViewModel:INotifyPropertyChanged
{
private Product _SelectedProduct;
public Product SelectedProduct
{
get { return _SelectedProduct; }
set
{
_SelectedProduct = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedProduct"));
_SelectedProductID = _SelectedProduct.ID;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedProductID"));
}
}
private int _SelectedProductID;
public int SelectedProductID
{
get { return _SelectedProductID; }
set
{
_SelectedProductID = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedProductID"));
_SelectedProduct = _AvailableProducts.FirstOrDefault(p => p.ID == value);
PropertyChanged(this,new PropertyChangedEventArgs("SelectedProduct"));
}
}
private IEnumerable<Product> _AvailableProducts = GetAvailableProducts();
private static IEnumerable<Product> GetAvailableProducts()
{
return new List<Product>
{
new Product{ID=1, ProductName = "Coke"},
new Product{ID = 2, ProductName="Sprite"},
new Product{ID = 3, ProductName = "Vault"},
new Product{ID=4, ProductName = "Barq's"}
};
}
public IEnumerable<Product> AvailableProducts
{
get { return _AvailableProducts; }
}
private Customer _SelectedCustomer;
public Customer SelectedCustomer
{
get { return _SelectedCustomer; }
set
{
_SelectedCustomer = value;
PropertyChanged(this, new PropertyChangedEventArgs("SelectedCustomer"));
SelectedProduct = value.FavoriteProduct;
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
So now your XAML binds to the appropriate properties and the viewModel is responsible for syncrhronization
<TextBox
x:Name="MyTextBox"
Text="{Binding Path=SelectedProductID, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox
x:Name="MyComboBox"
ItemsSource="{Binding AvailableProducts}"
DisplayMemberPath="ProductName"
SelectedItem="{Binding SelectedProduct}" />
Don't forget to implement the rest of INotifyPropertyChanged and the GetAvailableProducts function. Also there may be some errors. I hand typed this here instead of using VS but you should get the general idea.
Try:
SelectedItem="{Binding Path=YourPath, Mode=TwoWay"}
instead of setting SelectedValue and SelectedValuePath.
Might work with SelectedValue too, don't forget the Mode=TwoWay, since this isn't the default.
A good approuch would to use the master detail pattern - bind the master (the items view, e.g. combobox) to the data source collection and the detail view (e.g. text box) to the selected item in the source collection, using a binding converter to read/write the appropriate property.
Here is an example:
http://blogs.microsoft.co.il/blogs/tomershamam/archive/2008/03/28/63397.aspx
Notice the master binding is of the form {Binding} or {Binding SourceCollection} and the details binding is of the form {Binding } or {Binding SourceCollection}.
To get this working you need to wrap you collection with an object that keeps the selected item. WPF has one of these built-in: ObjectDataProvider.
Example:
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/068977c9-95a8-4b4a-9d38-b0cc36d06446