I have a few questions about a use of SelectMany I have encountered in one of the projects I am working on. Below is a small sample that reproduces its use (with a few Console.WriteLines I was using to help see the states at various points):
public partial class MainWindow : INotifyPropertyChanged
{
private bool _cb1, _cb2, _cb3, _isDirty;
private readonly ISubject<Unit> _cb1HasChanged = new Subject<Unit>();
private readonly ISubject<Unit> _cb2HasChanged = new Subject<Unit>();
private readonly ISubject<Unit> _cb3HasChanged = new Subject<Unit>();
private readonly ISubject<string> _initialState = new ReplaySubject<string>(1);
public MainWindow()
{
InitializeComponent();
DataContext = this;
ObserveCheckBoxes();
var initialState = string.Format("{0}{1}{2}", CB1, CB2, CB3);
_initialState.OnNext(initialState);
Console.WriteLine("INITIAL STATE: " + initialState);
}
public bool CB1
{
get
{
return _cb1;
}
set
{
_cb1 = value;
_cb1HasChanged.OnNext(Unit.Default);
}
}
public bool CB2
{
get
{
return _cb2;
}
set
{
_cb2 = value;
_cb2HasChanged.OnNext(Unit.Default);
}
}
public bool CB3
{
get
{
return _cb3;
}
set
{
_cb3 = value;
_cb3HasChanged.OnNext(Unit.Default);
}
}
public bool IsDirty
{
get
{
return _isDirty;
}
set
{
_isDirty = value;
OnPropertyChanged("IsDirty");
}
}
private void ObserveCheckBoxes()
{
var checkBoxChanges = new[]
{
_cb1HasChanged,
_cb2HasChanged,
_cb3HasChanged
}
.Merge();
var isDirty = _initialState.SelectMany(initialState => checkBoxChanges
.Select(_ => GetNewState(initialState))
.Select(updatedState => initialState != updatedState)
.StartWith(false)
.TakeUntil(_initialState.Skip(1)));
isDirty.Subscribe(d => IsDirty = d);
}
private string GetNewState(string initialState = null)
{
string update = string.Format("{0}{1}{2}", CB1, CB2, CB3);
if (initialState != null)
{
Console.WriteLine("CREATING UPDATE: " + update + " INITIAL STATE: " + initialState);
}
else
{
Console.WriteLine("CREATING UPDATE: " + update);
}
return update;
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
{
var newState = GetNewState();
_initialState.OnNext(newState);
Console.WriteLine("SAVED AS: " + newState);
}
}
and the xaml:
<Window x:Class="WpfSB2.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>
<StackPanel>
<CheckBox IsChecked="{Binding CB1}"></CheckBox>
<CheckBox IsChecked="{Binding CB2}"></CheckBox>
<CheckBox IsChecked="{Binding CB3}"></CheckBox>
<Button IsEnabled="{Binding IsDirty}" Click="Button_Click">APPLY</Button>
</StackPanel>
</Grid>
</Window>
So what this little app does is show three checkboxes (all initially unchecked) and an "Apply" button. When the state of the checkboxes changes, the button should become enabled and when clicked become disabled until the state of the checkboxes changes again. If you change the state of the checkboxes but then change it back to its inital state, the button will enable/disable appropriately. The app works as expected, I am just trying to figure out why/how.
Now the questions:
Will the SelectMany call be triggered whenever the _initialState or a check box change occurs?
The first call of _initialState.OnNext(initialState); (in the constructor) doesn't really do anything when it comes to the SelectMany code. I see it makes its way to the SelectMany code but nothing is actually done (I mean, if I put a breakpoint on the checkBoxChanges.Select section it breaks but nothing is actually selected). Is this because no changes have occured to any of the checkboxes yet?
As expected, checking any checkBox triggers the isDirty check. What exactly is happening in this SelectMany statement the first time I change a single checkbox?
After checking a box, the Apply button becomse enabled and I hit Apply. This causes _initialState.OnNext(newState); to be called. Similar to my first question, nothing seems to happen in the SelectMany statement. I thought with the initial state getting a new value something would get recalculated but it seems to go straight to the OnNext handler of isDirty.Subscribe(d => IsDirty = d);
Now that I hit Apply, _initialState.OnNext has been called twice in total. If I check a new checkbox, how does the SelectMany handle that? Does it go through all of the past states of _initialState? Are those values stored until the observable is disposed?
What are the StartsWith/TakeUntil/Skip lines doing? I noticed that if I remove the TakeUntil line, the app stops working correctly as the SelectMany clause starts going through all the past values of _initialState and gets confused as to which is the actual current state to compare to.
Please let me know if you need additional info.
I think the key part of your problem is your understanding of SelectMany. I think it is easier to understand if you refer to SelectMany as "From one, select many".
For each value from a source sequence, SelectMany will provide zero, one, or many values from another sequence.
In your case you have the source sequence that is _initialState. Each time a value is produced from that sequence it will subscribe to the "inner sequence" provided.
To directly answer your questions:
1) When _initialState pushes a value, then the value will be passed to the SelectMany operator and will subscribe to the provided "inner sequence".
2) The fist call is putting the InitialState in the ReplaySubject's buffer. This means when you first subscribe to the _initialState sequence it will push a value immediately. Putting your break point in the GetNewState will show you this working.
3) When you check a check box, it will call the setter, which will OnNext the _cbXHasChanged subject (yuck), which will flow into the Merged sequence (checkBoxChanges) and then flow into the SelectMany delegate query.
4) Nothing will happen until the check boxes push new values (they are not replaysubjects)
5-6) Yes you have called it twice so it will run the selectMany delegate twice, but the TakeUntil will terminate the first "inner sequence" when the second "inner sequence" is kicked off.
This is all covered in detail on (my site) IntroToRx.com in the SelectMany chapter.
Related
I have a combobox with a custom enum (just true/false). I have a function that checks conditions if the SelectedValue changes from false to true and if the conditions are wrong it changes the combobox SelectedValue back to false. This changes the SelectedValue to false if you check it in code, but when you look at the UI it's still on true.
Here's the xaml for the combobox:
<ComboBox x:Name="comboEnabled1" Width="80" Height="26"
ItemsSource="{Binding Path=TrueFalseChoices}"
SelectedValue="{Binding Path=Enable1, Mode=TwoWay}"/>
Here's the viewmodel
private TrueFalse _enable1 = TrueFalse.False;
public TrueFalse Enable1
{
get { return _enable1; }
set
{
if (_enable1 != value)
{
_enable1 = value;
base.OnPropertyChanged("Enable1");
OnEnableChanged(EventArgs.Empty);
}
}
}
And here's the function that I'm using to check the conditions
public void HandleEnable(object sender, EventArgs e)
{
if(Enable1 == TrueFalse.True)
{
if(!connected)
{
HandleMessage("Can't enable, not connected");
Enable1 = TrueFalse.False;
}
else if (!_main.CBCheck(_main.cbReason))
{
Enable1 = TrueFalse.False;
}
}
Console.WriteLine("Enabled {0}", Enable1);
}
Was thinking I'm changing the value too rapidly, but the last Console.Writeline produces the right outcome each time.
Any help appreciated!
Edit: Calling Handleenable here:
protected void OnEnableChanged(EventArgs e)
{
EventHandler handler = EnableChanged;
if (handler != null)
handler(this, e);
}
And in the ViewModel funct:
EnableChanged += HandleEnable;
Changing the Enable1 in any other place worked as it should have, only having issues in HandleEnable function.Also tried changing other comboboxes in the HandleEnable function and that worked as it should have.
I would recommend actually disabling the ComboBox if the requirements are not met.
But if you insist on reverting Enable1 back to False if conditions are not met, you should push the notification properly through the dispatcher.
set
{
var effectiveValue = condition ? value : TrueFalse.False;
if (effectiveValue == TrueFalse.False && value == TrueFalse.True)
System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(
new Action(() => base.OnPropertyChanged("Enable1"), null));
//your regular set-code follows here
}
It happens because WPF is already responding to that event, and therefore ignoring the subsequent calls until it's done. So you immediately queue another pass as soon as the current one is finished.
But I would still recommend disabling the ComboBox when it is effectively disabled. Accessing the dispatcher from a viewmodel does not smell good no matter how you look at it.
UPD: You can also solve that with {Binding Enable1, Delay=10} if your framework is 4.5.1+.
I have a DataGrid which looks like:
<DataGrid Grid.Row="3" Grid.Column="1" ItemsSource="{Binding Purchases}" SelectionMode="Single" SelectionUnit="FullRow"
SelectedItem="{Binding SelectedPurchase, Source={x:Static ex:ServiceLocator.Instance}}"
AutoGenerateColumns="False" CanUserAddRows="False">
<e:Interaction.Triggers>
<e:EventTrigger EventName="CellEditEnding">
<e:InvokeCommandAction Command="{Binding DataContext.CellEditEndingCommand,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Page}}}"/>
</e:EventTrigger>
</e:Interaction.Triggers>
<DataGrid.Columns>
.......
........
<DataGrid.Columns>
</DataGrid>
Property SelectedPurchase looks like:
private Purchase _selectedPurchase;
public Purchase SelectedPurchase
{
get
{
return _selectedPurchase;
}
set
{
_selectedPurchase = value;
NotifyPropertyChanged("SelectedPurchase");
}
}
CellEditEndingCommand
public ICommand CellEditEndingCommand { get; set; }
private void CellEditEndingMethod(object obj)
{
XDocument xmlPurchases = XDocument.Load(DirectoryPaths.DataDirectory + "Purchases.xml");
var currentPurchaseInData = (from purchase in xmlPurchases.Element("Purchases").Elements("Purchase")
where Convert.ToInt32(purchase.Attribute("Id").Value) == ServiceLocator.Instance.SelectedPurchase.Id
select purchase).FirstOrDefault();
currentPurchaseInData.SetElementValue("CreditorId", ServiceLocator.Instance.SelectedPurchase.Creditor.Id);
currentPurchaseInData.SetElementValue("AnimalId", ServiceLocator.Instance.SelectedPurchase.Animal.Id);
currentPurchaseInData.SetElementValue("QuantityInLitre", ServiceLocator.Instance.SelectedPurchase.Litre);
currentPurchaseInData.SetElementValue("FAT", ServiceLocator.Instance.SelectedPurchase.FAT);
currentPurchaseInData.SetElementValue("RatePerLitre", ServiceLocator.Instance.SelectedPurchase.RatePerLitre);
xmlPurchases.Save(DirectoryPaths.DataDirectory + "Purchases.xml");
}
Now If I change any value in DataGridCell and then I hit Enter CellEditEndingCommand is fired and CellEditEndingMethod is fired. But If I keep a breakpoint inside CellEditEndingMethod and take a look at it, then I can see that Values of any property of SelectedPurchase does not change to new values.
Let me give an example to explain the above line more correctly:
When I keep a breakpoint on any line inside CellEditEndingMethod and take a look at Properties like Litre, FAT etc., these properties values does not change. I mean I expect the property to take new value but it holds old value. Also, In view I can see the new values but in XML file there are still old values.
Update:
Purchases = new ObservableCollection<Purchase>(
from purchase in XDocument.Load(DirectoryPaths.DataDirectory + "Purchases.xml")
.Element("Purchases").Elements("Purchase")
select new Purchase
{
Id = Convert.ToInt32(purchase.Attribute("Id").Value),
Creditor = (
from creditor in XDocument.Load(DirectoryPaths.DataDirectory + "Creditors.xml")
.Element("Creditors").Elements("Creditor")
where creditor.Attribute("Id").Value == purchase.Element("CreditorId").Value
select new Creditor
{
Id = Convert.ToInt32(creditor.Attribute("Id").Value),
NameInEnglish = creditor.Element("NameInEnglish").Value,
NameInGujarati = creditor.Element("NameInGujarati").Value,
Gender = (
from gender in XDocument.Load(DirectoryPaths.DataDirectory + #"Basic\Genders.xml")
.Element("Genders").Elements("Gender")
where gender.Attribute("Id").Value == creditor.Element("GenderId").Value
select new Gender
{
Id = Convert.ToInt32(gender.Attribute("Id").Value),
Type = gender.Element("Type").Value,
ImageData = gender.Element("ImageData").Value
}
).FirstOrDefault(),
IsRegisteredMember = creditor.Element("IsRegisteredMember").Value == "Yes" ? true : false,
Address = creditor.Element("Address").Value,
City = creditor.Element("City").Value,
ContactNo1 = creditor.Element("ContactNo1").Value,
ContactNo2 = creditor.Element("ContactNo2").Value
}
).FirstOrDefault(),
Animal = (
from animal in XDocument.Load(DirectoryPaths.DataDirectory + #"Basic\Animals.xml")
.Element("Animals").Elements("Animal")
where animal.Attribute("Id").Value == purchase.Element("AnimalId").Value
select new Animal
{
Id = Convert.ToInt32(animal.Attribute("Id").Value),
Type = animal.Element("Type").Value,
ImageData = animal.Element("ImageData").Value,
Colour = animal.Element("Colour").Value
}
).FirstOrDefault(),
Litre = Convert.ToDouble(purchase.Element("QuantityInLitre").Value),
FAT = Convert.ToDouble(purchase.Element("FAT").Value),
RatePerLitre = Convert.ToDouble(purchase.Element("RatePerLitre").Value)
}
);
The CellEditEnding Event is not meant to update the datarow but to validate the single cell and keep it in editing mode if the content is not valid. The real update is done when the whole row is committed. Try it by adding the code in the HandleMainDataGridCellEditEnding method in http://codefluff.blogspot.de/2010/05/commiting-bound-cell-changes.html to your CellEditEndingMethod. It is good explained there. You may replace the if (!isManualEditCommit) {} by if (isManualEditCommit) return;.
UPDATE
You can extend your Purchase class by interface IEditableObject. DataGrid will call the method EndEdit() of this interface after the data has been committed and so you can do the XML stuff there. So you don't need any further buttons because a cell goes in edit mode automatically and the commit is done when you leave the row.
I think the CollectionChanged solution does not work because if you edit a dataset all changes take place inside the single object (Purchase) and not in the collection. CollectionChanged will be called by adding or removing an object to the collection
2nd UPDATE
Another try by putting it all together:
I simplified your Purchase class for demonstration:
class Purchase
{
public string FieldA { get; set; }
public string FieldB { get; set; }
}
Create a derived class to keep the real Purchase class clean:
class EditablePurchase : Purchase, IEditableObject
{
public Action<Purchase> Edited { get; set; }
private int numEdits;
public void BeginEdit()
{
numEdits++;
}
public void CancelEdit()
{
numEdits--;
}
public void EndEdit()
{
if (--numEdits == 0)
{
if (Edited != null)
Edited(this);
}
}
}
This is explained in SO WPF DataGrid calls BeginEdit on an IEditableObject two times?
And create the Purchases collection:
ObservableCollection<EditablePurchase> Purchases = new ObservableCollection<EditablePurchase>()
{
new EditablePurchase {FieldA = "Field_A_1", FieldB = "Field_B_1", Edited = UpdateAction},
new EditablePurchase {FieldA = "Field_A_2", FieldB = "Field_B_2", Edited = UpdateAction}
};
Purchases.CollectionChanged += Purchases_CollectionChanged;
private void Purchases_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (EditablePurchase item in e.NewItems)
item.Edited = UpdateAction;
}
void UpdateAction(Purchase purchase)
{
// Save XML
}
This provides that the calls to Edited are catched for all EditablePurchase elements from initialization and for newly created ones. Be sure to have the Edited property set in initializer
This is a disgrace for WPF. No DataGrid.CellEditEnded event? Ridiculous, and I didn't know about that so far. It's an interesting question.
As Fratyx mentioned, you can call
dataGrid.CommitEdit(DataGridEditingUnit.Row, true);
in a code behind CellEditEnding method. While it works, I find it's quite ugly. Not only because of having code behind (could use a behavior to circumnavigate that), but your ViewModel CellEditEndingMethod will be called twice, once for no good reason because the edit is not yet committed.
I would probably opt to implement INotifyPropertyChanged in your Purchase class (I recommend using a base class so you can write properties on one line again) if you haven't already, and use the PropertyChanged event instead:
public MyViewModel()
{
Purchases = new ObservableCollection<Purchase>();
Purchases.CollectionChanged += Purchases_CollectionChanged;
}
private void Purchases_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (Purchase item in e.NewItems)
item.PropertyChanged += Purchase_PropertyChanged;
if (e.OldItems != null)
foreach (Purchase item in e.OldItems)
item.PropertyChanged -= Purchase_PropertyChanged;
}
private void Purchase_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// save the xml...
}
You will not get any CollectionChanged event before DataGrid is changing the collection. And this does not before happen a dataset is committed.
If you press 'Enter' in a cell you change the value of this cell in a kind of copy of the real dataset. So it is possible to skip the changes by rollback. Only after finishing a row e.g. by changing to another row or direct commit your changed data will be written back to the original data. THEN the bindings will be updated and the collection is changed.
If you want to have an update cell by cell you have to force the commit as in the code I suggested.
But if you want to have a puristic MVVM solution without code behind you have to be content with the behavior DataGrid is intended for. And that is to update after row is finished.
public string TenNguoiDung
{
get { return _currentNguoiDung.TenNguoiDung; }
set
{
_currentNguoiDung.TenNguoiDung = value != null
? value.ToStandardString(true) : string.Empty;
SendPropertyChanged("TenNguoiDung");
ValidProperty(_currentNguoiDung.TenNguoiDung, new
ValidationContext(_currentNguoiDung) { MemberName = "TenNguoiDung" });
}
}
public static string ToStandardString(this string value,
bool isAllStartWithUpper = false)
{
string result = string.Empty;
value = value.Trim();
var listWord = value.Split(' ').ToList();
listWord.RemoveAll(p => p == string.Empty);
foreach (var item in listWord) result +=
item.Substring(0, 1).ToUpper() + item.Substring(1).ToLower() + " ";
if (!isAllStartWithUpper) result =
result.Substring(0, 1) + result.Substring(1).ToLower();
return result.Trim();
}
I have a TextBox:
<TextBox Grid.Column="1" Margin="1" Text=
"{Binding Path=TenNguoiDung,Mode=TwoWay,NotifyOnValidationError=True}"/>
When i typed some clear text to Standard, Setter called SendPropertyChanged("TenNguoiDung") but on UI not update. How can i fix that??
Edit:
public void SendPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Edit:
I debuged at Setter, i saw _currentNguoiDung.TenNguoiDung changed after ToStandardString, but on UI was not updated
Update
I used few hours to google and i got my answer.
I feel happy when this bug only happen on debug mode. When i run application without debug, it work perfect!!
Fix debug Mode:
[link]Coerce Value in Property Setter - Silverlight 5
Right click on the Web project in Solution Explorer and select
Properties.
Select the Web tab.
Scroll down to the Debuggers section.
Uncheck the checkbox labelled Silverlight.
Make sure you are setting the data context to the instance of the class containing your property.
From the looks of it you are doing this but make sure you are implementing INotifyPropertyChanged
When debugging if you have any binding errors happening these will show in the output window.
Is the value changing if you type in 'hello' and then replace it with 'world'? If so it may just be a case sensitivity issue
I am not understanding the following 4 points about databinding. Not sure if i get unexpected results (outdated data in code behind), or if this is just a result of me misunderstanding how things work.
Bound data to a textbox updates when i leave the textbox. What event
EXACTLY does this? LostFocus?
When using the now changed data in code behind
it seems to still use the OLD data. Why is this happening? Could
point 3 be the reason?
After the textbox updates i did a test and set datacontext
to nothing and reaplied datacontext to the same scource. The values
shown are the values before i edited them. Why did they show up after editing, but
returned to the old values after rebinding?
After changing the values for the second time it seems
like code behind uses the data after my first change. Rebinding like
in point 3 leads to the same result (value after first change,
second change ignored). Seems like code behind is always one update behind, can i change this?
Anyone able to explain why this happens?
Desired behavior:
I want the people count to update when I edit the housing count. Preferable on the fly, but after losing focus is fine. When losing focus the value for isle ID 0 should be the right one tho, and not the outdated value.
For easier understanding, a picture with 3 screens and related code samples.
http://www.mathematik-lehramtsstudium.de/BindingExample.jpg
My class:
//class for isles
public class isle : INotifyPropertyChanged
{
//Dummyvariables,...
private int _intBauer;
private int _intBauerBev;
//variables
public int intIsleID { set; get; } //isle ID
public string strName { set; get; } //isle name
public int intBauer //housing count
{
set
{
this._intBauer = value;
NotifyPropertyChanged("intBauer"); NotifyPropertyChanged("intBauerBev");
}
get
{
return _intBauer;
}
}
public int intBauerBev //each house hosts 8 people
{
set { this._intBauerBev = value;}
get { return intBauer * 8; }
}
protected void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
LostFocus-Event for updating the right side oft he page
private void textboxHäuser_LostFocus(object sender, RoutedEventArgs e)
{
//Gesamtzahl neu berechnen
listIsles[0].intBauer = 0;
for (int i = 1; i < 10; i++)
{
listIsles[0].intBauer += listIsles[i].intBauer;
}
//hard refresh DataContext, since it does not seem to update itself
//leaving these two lines out of my code changes nothing tho, as expected
gridInfoGesamt.DataContext = "";
gridInfoGesamt.DataContext = listIsles[0];
}
The issue i was facing is the order in which events get fired in this case. Or more accurate: Two things happening at once.
TextBox uses the "LostFocus"-Event to update the property, same as the event i used to update my other TextBox controls. Since both fired at once i used the "outdated" data for calculations, therefore it looked like my UI lagged one step behind on one side.
To fix this i simply had to change the way my TextBox updates the property, by doing my binding like this in XAML:
Text="{Binding intBauer, UpdateSourceTrigger=PropertyChanged}"
Now the property is updated instantly, before "LostFocus" and even before "TextChanged".
This also opens the possibility to update the UI as the user changes values and not only after he is finished. Much cleaner and better looking.
I have overrided the method OnSourceInitialized and I have one problem. After populating my combobox with source property from c# code I want automatically an item will appear selected in the combobox when a page is loaded (default value) but for some reason after onsourceinitialized method, the combobox selected item change to null.
EDIT
First of all, very good explanation thanks.
I'll try to explain more and I post some code following. I have made some modifications but without success. It continues not working.
My goal is to show a default value selected in the combobox when window is loaded and it is shown.
Initially, when user selects a option in menu application I do the following:
WinMain.xaml.cs:
namespace MyNamespace
{
public partial class WinMain : Window
{
<...>
private void mnuItemPreferences_Click(object sender, RoutedEventArgs e)
{
MyNamespace.Windows.EditPreferences editPrefWnd =
new MyNamesapece.Windows.EditPreferences();
//
// Modal window that I want to open with default values in comboboxes
//
editPrefWnd.ShowDialog();
}
<...>
} // end WinMain class
} // end namespace
EditPreferences.xaml.cs:
namespace MyNamespace.Windows
{
public partial class EditPreferences : Window
{
<...>
// My constructor
public EditPreferences()
{
//
// Handlers
//
Loaded += PreferencesWindow_Loaded;
Closing += PreferencesWindow_Closing;
InitializeComponent();
if (System.Environment.OSVersion.Version.Major < 6)
{
this.AllowsTransparency = true;
_bolAeroGlassEnabled = false;
}
else
{
_bolAeroGlassEnabled = true;
}
this.ShowInTaskbar = false;
} // end constructor
private void PreferencesWindow_Loaded(object sender,
System.Windows.RoutedEventArgs e)
{
if (this.ResizeMode != System.Windows.ResizeMode.NoResize)
{
//this work around is necessary when glass is enabled and the
//window style is None which removes the chrome because the
//resize mode MUST be set to CanResize or else glass won't display
this.MinHeight = this.ActualHeight;
this.MaxHeight = this.ActualHeight;
this.MinWidth = this.ActualWidth;
this.MaxWidth = this.ActualWidth;
}
//
// Populate comboboxes
//
cbLimHorasExtra.ItemsSource = Accessor.GetLimHorasExtraSorted();
cbFracHorasExtra.ItemsSource = Accessor.GetFracHorasExtraSorted();
//
// Fill controls with default values (see below)
//
FillControls();
//
// Install other handlers
//
rdoBtnOTE.Checked += this.rdoBtnOTE_Checked;
rdoBtnOTM.Checked += this.rdoBtnOTM_Checked;
chkboxRestrict.Checked += this.chkboxRestrict_Checked;
expAdditionalDetails.Collapsed +=
this.expAdditionalDetails_Collapsed;
expAdditionalDetails.Expanded += this.expAdditionalDetails_Expanded;
cbLimHorasExtra.SelectionChanged +=
this.cbLimHorasExtra_SelectionChanged;
cbFracHorasExtra.SelectionChanged +=
this.cbFracHorasExtra_SelectionChanged;
}
protected override void OnSourceInitialized(System.EventArgs e)
{
base.OnSourceInitialized(e);
if (_bolAeroGlassEnabled == false)
{
//no aero glass
this.borderCustomDialog.Background =
System.Windows.SystemColors.ActiveCaptionBrush;
this.tbCaption.Foreground =
System.Windows.SystemColors.ActiveCaptionTextBrush;
this.borderCustomDialog.CornerRadius =
new CornerRadius(10, 10, 0, 0);
this.borderCustomDialog.Padding =
new Thickness(4, 0, 4, 4);
this.borderCustomDialog.BorderThickness =
new Thickness(0, 0, 1, 1);
this.borderCustomDialog.BorderBrush =
System.Windows.Media.Brushes.Black;
}
else
{
//aero glass
if (VistaAeroAPI.ExtendGlassFrame(this,
new Thickness(0, 25, 0, 0)) == false)
{
//aero didn't work make window without glass
this.borderCustomDialog.Background =
System.Windows.SystemColors.ActiveCaptionBrush;
this.tbCaption.Foreground =
System.Windows.SystemColors.ActiveCaptionTextBrush;
this.borderCustomDialog.Padding =
new Thickness(4, 0, 4, 4);
this.borderCustomDialog.BorderThickness =
new Thickness(0, 0, 1, 1);
this.borderCustomDialog.BorderBrush =
System.Windows.Media.Brushes.Black;
_bolAeroGlassEnabled = false;
}
}
}
private void FillControls()
{
tblPreferencias tbl_pref = null;
//
// Obtain data (a record with fields)
// Accessor is a class where I define the methods to
// obtain data of different tables in my database
//
tbl_pref = Accessor.GetActualPreferencias();
//
// Only returns one register
//
if (tbl_pref != null)
{
rdoBtnOTE.IsChecked = (bool)tbl_pref.OTE;
rdoBtnOTM.IsChecked = (bool)tbl_pref.OTM;
chkboxRestrict.IsChecked =
(bool)tbl_pref.RestriccionHExtraTipoA;
// Here the value assigned is always in the range of the values
// which combo has been populated.
// With one 0 ... 8
// I debbugged it and works.
// selected value (no null) and text gets the correct value I
// want but after OnSourceInitialized method is executed I note
// that for some rease selected value property gets value null
cbLimHorasExtra.Text = tbl_pref.LimiteHorasExtra.ToString();
cbFracHorasExtra.Text =
tbl_pref.FraccionDeMinutosExtra.ToString();
}
}
<...>
} // end EditPreferences class
} // end namespace
EditPreferences.xaml (I put as example one of the comboboxes):
<Window x:Class="MyNamespace.Windows.EditPreferences"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="EditPreferences" Height="Auto" Width="500"
Background="{x:Null}"
SnapsToDevicePixels="True" SizeToContent="Height"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
WindowStyle="None"
Margin="0,0,0,0"
>
<...>
<ComboBox x:Name="cbLimHorasExtra"
DisplayMemberPath="LimHora"
SelectedValuePath="Id"
SelectedItem="{Binding Path=Id}"
VerticalAlignment="Center"
HorizontalContentAlignment="Right"
Width="50"/>
<...>
</Window>
Accessor.cs:
namespace GesHoras.Classes
{
class Accessor
{
<...>
// This method is used to populate the combobox with its values
// tblLimHorasExtra is a table in my SQL Database
// Its fields are:
//
// Id : int no null (numbers 1 ... 9)
// LimHora: int no null (numbers 0 ... 8)
//
public static System.Collections.IEnumerable GetLimHorasExtraSorted()
{
DataClassesBBDDDataContext dc = new
DataClassesBBDDDataContext();
return (from l in dc.GetTable<tblLimHorasExtra>()
orderby l.LimHora
select new { Id=l.Id, LimHora=l.LimHora });
}
// tblPreferencias is a table in my SQL Database
// Its fields are:
//
// Id : int no null
// Descripcion : varchar(50) no null
// OTE : bit no null
// OTM : bit no null
// LimiteHorasExtra : int no null
// FraccionDeMinutosExtra : int no null
// RestriccionHExtraTipoA : bit no null
//
public static tblPreferencias GetActualPreferencias()
{
DataClassesBBDDDataContext dc = new
DataClassesBBDDDataContext();
return (from actP in dc.GetTable<tblPreferencias>()
where (actP.Id == 3)
select actP).SingleOrDefault<tblPreferencias>();
}
<...>
} // end class
} // end namespace
The problem I see is that when method fillControls is executed all is ok, selectedvalue and text property for the combobox is correct (I have debbugged it and is correct) but after executing OnSourceInitialized method, selectedvalue property for the combobox gets null value.
Also I note that, when window opens, the comboboxes appear with the default values selected that I want but quickly I see that for some reason their values selected turns to empty in the comboboxes. It's like some event (I think after executing OnSourceMethod because I have debugged and see how it change to null) makes the selected default values that appears ok in the comboboxes turn to empty.
I have tested that comboboxes are populated correctly because once the window is shown I click in the comboboxes and I can see they are populated ok.
EDIT 2
Also I have forced selected index for combobox in fillControls method by doing:
cbLimHorasExtra.SelectedIndex = 1;
but without success...
The combobox is populated with values: 0 to 8 both included.
Cause
This appears to be the problem:
SelectedItem="{Binding Path=Id}"
If the "Id" property in the DataContext is not an item in the ItemsSource, SelectedItem will be set to null.
Timing
When InitializeComponent is called, it parses the XAML which sets the SelectedItem binding. If the DataContext is not set, then initially this will be null. Later when DataContext is set, the binding is re-evaluated. If Id is in the list at that point, the SelectedItem is set. Otherwise it is set to null.
Any binding that cannot be evaluated initially during InitializeComponent is scheduled using the dispatcher to be re-evaluated once all events have fired. Without details on how your DataContext is being set I can't give specifics, but my guess is that one of your binding is getting deferred so your {Binding Path=Id} binding is evaluated in a dispatcher callback.
A dispatcher callback is not an event - it is a prioritized work queue. If you have this kind of situations your choices are:
Change the bindings so they can be evaluated during initialization
Use a Dispather.BeginInvoke to schedule your own callback to execute after the Binding completes
Let the Binding take care of setting the SelectedItem rather than setting manually in code
Additional notes
Your use of SelectedValueSource looks suspicious. Your binding to SelectedItem seems to indicate that each item in the ItemsSource is an "Id", but your definition of SelectedValueSource seems to indicate that each item in the ItemsSource contains an "Id". It is rare to find a data structure where the structure itself is called "Id" by another structure, yet it itself has an "Id" field. Thus I suspect some confusion here. Without seeing your actual data structures I can't say more.
Your use of OnSourceInitialized also makes it appear you have a misunderstanding. The "Source" in the name of OnSourceInitialized refers to a "presentation source" such as a Win32 hWnd, not a source of data. The purpose of OnSourceInitialized is to interact at a low level with the Windows operating system, or to update your application based on where it is being presented. Your use seems completely unrelated to this. I would recommend you stay away from OnSourceInitialized. Generally the best time to initialize ComboBoxes and such is to just provide it in your view model and let data binding take care of it. As soon as the view model is available the data will be populated with no code required.
Set the SelectedIndex property at the end of your override, by the way, i can't seem to find OnSourceInitialised, only Initialised. But it should still work if you set it at the end of your code.
private void MyListBox_Initialized(object sender, EventArgs e)
{
// Run some code
if (MyListBox.Items.Count > 0)
{
MyListBox.SelectedIndex = 0;
}
}
I don't have a real answer to your question, but OnSourceInitialized seems to be too early in the initialization process.
Again, I have not tried your exact scenario, but many problems like this one are solved by calling FillControls (i.e. setting the selected item) in the Loaded event instead of earlier.
I have solved it!
The problem was in binding the SelectedItem property in EditPreferences.xaml:
<ComboBox x:Name="cbLimHorasExtra"
DisplayMemberPath="LimHora"
SelectedValuePath="Id"
SelectedItem="{Binding Path=Id}"
VerticalAlignment="Center"
HorizontalContentAlignment="Right"
Width="50"/>
The solution is to change to:
<ComboBox x:Name="cbLimHorasExtra"
DisplayMemberPath="LimHora"
SelectedValuePath="Id"
SelectedItem="Id"
VerticalAlignment="Center"
HorizontalContentAlignment="Right"
Width="50"/>