Passing application state between viewmodels in MVVM WPF application - c#

I need to write a small application to read a configuration file and generate some report with it. I was hoping to finally use MVVM but it's quite tricky to get started. Oh, I'm using Caliburn.Micro framework.
So this is what I have, a shell (primary view that hosts other views) that has a ribbon with 3 buttons on it:
1) Open file
2) Show settings
3) Show results
And two other views, SettingsView and ResultsView with buttons to generate and delete a report.
So I guess the view structure would be like this:
ShellView
Ribbon
OpenFileButton
SettingsButton
ResultsButton
ContentControl (hosts SettingsView and ResultsView)
SettingsView
CalculateResultsButton
ResultsView
CancelResultsButton
The tricky part is this:
1. "Show settings" button is disabled until a file is opened (via Open file).
2. "Show results" button is disabled until a report is calculated (via a
method in SettingsViewModel).
3. If a report is calculated, the CalculateResultsButton is disabled and
CancelResultsButton is enabled and vice versa.
Please advise how could I achieve this ? I've no ideas what strategy should I go for. My non-MVVM-thinking-brain says that I should create a status variable and then somehow bind those buttons to that variable, but I guess that wont work in a MVVM world, right ? Any code example would be very very very appreciated!
Many thanks!

Since you're using CM you won't need any code-behind. You can delete the .xaml.cs files if you want.
This is a pretty basic example but it should give you an idea on how to control the state of the buttons. In this example, Open will be enabled and the other two are disabled. If you click on Open, Settings is enabled. The same happens with Results once Settings is clicked.
If you need a way to do global state the same concept can be applied by injecting a singleton, SharedViewModel, into the ViewModels and the CanXXX methods can check values in SharedViewModel. This is a SL demo of different things but one is injecting a singleton to share data, the same idea applies in wpf.
ShellView:
<Window x:Class="CMWPFGuardSample.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0"
Orientation="Horizontal">
<Button x:Name="Open"
Content="Open" />
<Button x:Name="Settings"
Content="Settings" />
<Button x:Name="Results"
Content="Results" />
</StackPanel>
</Grid>
</Window>
ShellViewModel:
[Export(typeof (IShell))]
public class ShellViewModel : PropertyChangedBase, IShell
{
private bool _isOpen;
public bool IsOpen
{
get { return _isOpen; }
set
{
_isOpen = value;
NotifyOfPropertyChange(() => IsOpen);
NotifyOfPropertyChange(() => CanSettings);
}
}
private bool _isSettings;
public bool IsSettings
{
get { return _isSettings; }
set
{
_isSettings = value;
NotifyOfPropertyChange(() => IsSettings);
NotifyOfPropertyChange(() => CanResults);
}
}
public bool IsResults { get; set; }
public void Open()
{
IsOpen = true;
}
public bool CanSettings
{
get { return IsOpen; }
}
public void Settings()
{
IsSettings = true;
}
public bool CanResults
{
get { return IsSettings; }
}
public void Results()
{
}
}

MVVM and WPF Commands perfectly fits your "tricky part" requirements since have built in ICommand.CanExecute() method which allows enabling/disabling corresponding button based on custom logic.
To use this naice feature take a look first at the RoutedCommand Class and self explanatory example on MSDN How to: Enable a Command (see below code snippets).
And in general about MVVM, it is really SIMPLE! Just try it and you won't leave without it ;) In few words - you have to create for each EntityView.xaml corresponding EntityViewModel class and then just put instance of it in the View's DataContext either explicitly in code or using bindings:
var entityViewModel = new EntityViewModel();
var view = new EntityView();
view.DataContext = entityViewModel;
MVVM Command and Command.CanExecute bindings:
XAML:
<Window x:Class="WCSamples.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="CloseCommand"
Name="RootWindow"
>
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Close"
Executed="CloseCommandHandler"
CanExecute="CanExecuteHandler"
/>
</Window.CommandBindings>
<StackPanel Name="MainStackPanel">
<Button Command="ApplicationCommands.Close"
Content="Close File" />
</StackPanel>
</Window>
C# code behind:
// Create ui elements.
StackPanel CloseCmdStackPanel = new StackPanel();
Button CloseCmdButton = new Button();
CloseCmdStackPanel.Children.Add(CloseCmdButton);
// Set Button's properties.
CloseCmdButton.Content = "Close File";
CloseCmdButton.Command = ApplicationCommands.Close;
// Create the CommandBinding.
CommandBinding CloseCommandBinding = new CommandBinding(
ApplicationCommands.Close, CloseCommandHandler, CanExecuteHandler);
// Add the CommandBinding to the root Window.
RootWindow.CommandBindings.Add(CloseCommandBinding);

Related

KeyBinding works as UserControl but not when pusing property element syntax in XAML

As title states, I can't get KeyBinding to work when using property element syntax. By work I mean using the key combo of Ctrl+Del to change the background color of the list box. The key combo can be used or the button can be clicked, both of which invoke the command, yet the command is never invoked. When a breakpoint is set while in debug mode it will never be encountered.
I've followed the InputBinding Class example from the documentation and can only get KeyBinding to work when using a UserControl and would like to understand why that is, and what I'm doing wrong.
Below is an MVCE of when the code, declared with property element syntax, that does not work. Commented out is a line for a UserControl which encapsulates the StackPanel and allows the KeyBinding to work. Contingent on the commenting out each PropertyElementSyntax region and uncommenting each UserControlSyntax region in the code behind for MainWindow.xaml.cs.
MainWindow.xaml:
<Window x:Class="LearningKeyBindingWPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:LearningKeyBindingWPFApp"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="300">
<!--<local:UserControl1 x:Name="CustomColorPicker" />-->
<StackPanel Margin="0,40,0,0">
<StackPanel.InputBindings>
<KeyBinding Command="{Binding ChangeColorCommand}"
CommandParameter="{Binding ElementName=ColorPicker, Path=SelectedItem}"
Key="{Binding ChangeColorCommand.Key}"
Modifiers="{Binding ChangeColorCommand.ModifierKeys}" />
<MouseBinding Command="{Binding ChangeColorCommand}"
CommandParameter="{Binding ElementName=ColorPicker, Path=SelectedItem}"
MouseAction="{Binding ChangeColorCommand.MouseAction}" />
</StackPanel.InputBindings>
<Button Content="Change Color"
Command="{Binding ChangeColorCommand}"
CommandParameter="{Binding ElementName=ColorPicker, Path=SelectedItem}" />
<ListBox Name="ColorPicker"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
SelectedIndex="0">
<sys:String>Red</sys:String>
<sys:String>Green</sys:String>
<sys:String>Blue</sys:String>
<sys:String>Yellow</sys:String>
<sys:String>Orange</sys:String>
<sys:String>Purple</sys:String>
</ListBox>
</StackPanel>
</Window>
Code-behind for MainWindow.xaml.cs:
public MainWindow()
{
DataContext = this;
InitializeComponent();
InitializeCommand();
#region UserControlSyntax
//CustomColorPicker.ColorPicker.Focus();
#endregion
#region PropertyElementSyntax
ColorPicker.Focus();
#endregion
}
public SimpleDelegateCommand ChangeColorCommand { get; private set; }
private SolidColorBrush _originalColor;
private void InitializeCommand()
{
#region UserControlSyntax
//_originalColor = (SolidColorBrush)CustomColorPicker.ColorPicker.Background;
#endregion
#region PropertyElementSyntax
_originalColor = (SolidColorBrush)ColorPicker.Background;
#endregion
ChangeColorCommand = new SimpleDelegateCommand(ChangeColor)
{
Key = Key.Delete,
ModifierKeys = ModifierKeys.Control
};
}
private void ChangeColor(object colorString)
{
if (colorString == null)
{
return;
}
var selectedColor = SelectedColor((string)colorString);
#region UserControlSyntax
//if (CustomColorPicker.ColorPicker.Background == null)
//{
// CustomColorPicker.ColorPicker.Background = selectedColor;
// return;
//}
//CustomColorPicker.ColorPicker.Background = ((SolidColorBrush)CustomColorPicker.ColorPicker.Background).Color == selectedColor.Color
// ? _originalColor
// : selectedColor;
#endregion
#region PropertyElementSyntax
if (ColorPicker.Background == null)
{
ColorPicker.Background = selectedColor;
return;
}
var isColorIdentical = ((SolidColorBrush)ColorPicker.Background).Color == selectedColor.Color;
ColorPicker.Background = isColorIdentical
? _originalColor
: selectedColor;
#endregion
}
private SolidColorBrush SelectedColor(string value)
{
#region UserControlSyntax
//var selectedColor = (Color)ColorConverter.ConvertFromString(value);
#endregion
#region PropertyElementSyntax
var selectedColor = (Color)ColorConverter.ConvertFromString((string)ColorPicker.SelectedItem);
#endregion
return new SolidColorBrush(selectedColor);
}
The problem is that in the no-UserControl scenario, the DataContext is set before the command object has been initialized.
WPF has a robust binding system, but it normally relies on property-change notifications, via INotifyPropertyChanged. Some scenarios will work without that, as long as you get the order of operations correct. But, without property-change notifications, if you miss your window of opportunity to present some property value to WPF, it's not going to try again later.
When you use the UserControl, the initialization of the bindings for the UserControl occurs after you set up the ChangeColorCommand property. This is just an artifact of how WPF initializes the various objects in the UI tree. But it means that by the time the UserControl's bindings look at the ChangeColorCommand property, it has the value you want.
On the other hand, when you put the StackPanel explicitly into the window's XAML, it's too late by the time you set the property for WPF to see it. It already resolved those bindings during the InitializeComponent() call. Setting the property later has no effect.
There are a couple of ways you could address that given the code you have now:
The simplest is to just move the assignment of DataContext = this; to after the call to InitializeCommand(). Updating the DataContext requires WPF to update all of the dependent bindings too, so doing that after the InitializeCommand() call ensures the property has the value you want.
Implement INotifyPropertyChanged in the MainWindow class, and raise the PropertyChanged event for the ChangeColorCommand property when you set it. This will let WPF know that the value changed and that it should re-evaluate any bindings that depended on it.
All that said, I'd go one further:
Implement a proper view model object, with INotifyPropertyChanged and a ChangeColorCommand, and use that as the data context. Making your UI objects do double-duty as both UI and property binding source (i.e. the view model's job) doesn't fit with the normal WPF model, sacrifices the benefits that MVVM would normally provide, and of course introduces this kind of weird timing thing where it's not obvious why a property binding isn't working as expected.
Okay, technically there's a fourth approach you could take, which is to put the call to InitializeCommand() before InitializeComponent(). Main problem with that is, at the moment, it relies on retrieving directly the value of a UI object's property, and that UI object won't exist until after InitializeComponent() is called.
Which brings me back to the #3 option above. Fact is, you shouldn't be accessing UI object properties directly. That should be another property in your view model, and you should make a more direct choice about what that initial color should be, than just grabbing it from the UI on startup.
I admit, there's some wiggle room for design here, but you should be trying to keep your view model and UI code as divorced from each other as possible.

AvalonDock MVVM Anchorable Location

I have an AvalonDock (version 3.5) in my MVVM WPF application. The binding is as follows:
<xcad:DockingManager Name="_dockingManager" Grid.Row="1"
DataContext="{Binding DockingManagerViewModel}"
DocumentsSource="{Binding Documents}"
AnchorablesSource="{Binding Anchorables}" >
So when I add a new Anchorable to the corresponding "Anchorables" collection in my view model the corresponding view shows up. Nevertheless the views always show up docked on the right side of my application. How can I control that the view is docked on the left side of my application via the view model?
I don't think you can control this in you viewmodel.
There 2 ways for controlling this.
You can restore the layout from a previously saved (default) layout whenever the
application is started for te first time or
You can setup the XAML to use an initial layout as you wish (prefered solution)
For the second option:
You can use the XAML binding in the DockingManager class to implement your requirement:
See TestApp sample for full implementation of the below snippet (just change LeftSide to BottomSide to see the effect):
<avalonDock:DockingManager Grid.Row="1">
...
<avalonDock:LayoutRoot.LeftSide>
<avalonDock:LayoutAnchorSide>
<avalonDock:LayoutAnchorGroup>
<avalonDock:LayoutAnchorable Title="AutoHide1 Content" ContentId="AutoHide1Content" IconSource="/AvalonDock.TestApp;component/Images/address-book--pencil.png" >
<TextBox Text="{Binding TestTimer, Mode=OneWay, StringFormat='AutoHide Attached to Timer ->\{0\}'}"/>
</avalonDock:LayoutAnchorable>
<avalonDock:LayoutAnchorable Title="AutoHide2 Content" ContentId="AutoHide2Content">
<StackPanel Orientation="Vertical">
<TextBox/>
<TextBox/>
</StackPanel>
</avalonDock:LayoutAnchorable>
</avalonDock:LayoutAnchorGroup>
</avalonDock:LayoutAnchorSide>
</avalonDock:LayoutRoot.LeftSide>
</avalonDock:LayoutRoot>
</avalonDock:DockingManager>
You can add a property (call it InitialPosition, or something like that) to your Anchorable view model, and implement a ILayoutUpdateStrategy to position the anchorable on the left, right or bottom side.
Add something like this to your XAML:
<xcad:DockingManager …>
…
<xcad:DockingManager.LayoutUpdateStrategy>
<local:LayoutUpdate />
</xcad:DockingManager.LayoutUpdateStrategy>
</xcad:DockingManager>
and your LayoutUpdate class:
class LayoutUpdate: ILayoutUpdateStrategy
{
static Dictionary<PaneLocation, string> _paneNames = new Dictionary<PaneLocation, string>
{
{ PaneLocation.Left, "LeftPane" },
{ PaneLocation.Right, "RightPane" },
{ PaneLocation.Bottom, "BottomPane" },
};
public bool BeforeInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorableToShow, ILayoutContainer destinationContainer)
{
if (anchorableToShow.Content is IAnchorable anch)
{
var initLocation = anch.InitialLocation;
string paneName = _paneNames[initLocation];
var anchPane = layout.Descendents()
.OfType<LayoutAnchorablePane>()
.FirstOrDefault(d => d.Name == paneName);
if (anchPane == null)
{
anchPane = CreateAnchorablePane(layout, Orientation.Horizontal, initLocation);
}
anchPane.Children.Add(anchorableToShow);
return true;
}
return false;
}
static LayoutAnchorablePane CreateAnchorablePane(LayoutRoot layout, Orientation orientation,
PaneLocation initLocation)
{
var parent = layout.Descendents().OfType<LayoutPanel>().First(d => d.Orientation == orientation);
string paneName = _paneNames[initLocation];
var toolsPane = new LayoutAnchorablePane { Name = paneName };
if (initLocation == PaneLocation.Left)
parent.InsertChildAt(0, toolsPane);
else
parent.Children.Add(toolsPane);
return toolsPane;
}
public void AfterInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorable)
{
// here set the initial dimensions (DockWidth or DockHeight, depending on location) of your anchorable
}
This code is extracted and changed a bit from the working application, with different types and names. It should probably work, but there might be a typo or other error somewhere.

ListView not updating on CollectionChanged

I am starting to play with Realm, and I am trying to bind a collection from the Realm database to a ListView. The binding works fine, but my ListView does not update when adding new items. My understanding is that IRealmCollection<> implements INotifyCollectionChanged and INotifyPropertyChanged events.
Here is a simple application to reproduce the issue:
View:
<Page x:Class="App3.MainPage"
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:local="using:App3"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<Button Click="ButtonBase_OnClick" Content="Add" />
<ListView x:Name="ListView">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Id}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Grid>
</Page>
CodeBehind:
namespace App3
{
public class Thing : RealmObject
{
public string Id { get; set; } = Guid.NewGuid().ToString();
}
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
private Realm _realm;
private IRealmCollection<Thing> things;
public MainPage()
{
this.InitializeComponent();
_realm = Realm.GetInstance();
things = (IRealmCollection<Thing>)_realm.All<Thing>();
ListView.ItemsSource = things;
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
_realm.Write(() =>
{
var thing = new Thing();
_realm.Add(thing);
});
}
}
}
I normally use MVVM (Template10), but this is a simple application to demonstrate the issue. Clicking the Add button adds an item to the database, but the ListView only updates when the application is first loaded. I have read similar questions, but I have not been able to find an answer that works yet. Inverse Relationships and UI-Update not working? is the closest I have found yet, but still does not fix the issue.
EDIT
I can force it to rebind like so:
ListView.ItemsSource = null;
ListView.ItemsSource = things;
But that is not optimal. I am trying to take advantage of Realm's "live objects" where the collection should always know when items are changed or added.
EDIT 2
Setting BindingMode=OneWay in code-behind also does not change the behavior:
_realm = Realm.GetInstance();
things = (IRealmCollection<Thing>)_realm.All<Thing>();
var binding = new Binding
{
Source = things,
Mode = BindingMode.OneWay
};
ListView.SetBinding(ListView.ItemsSourceProperty, binding);
SOLUTION
It turned out to be a known issue in IRealmCollection: https://github.com/realm/realm-dotnet/issues/1461#issuecomment-312489046 which is fixed in Realm 1.6.0. I have updated to the pre-release NuGet package and can confirm that the ListView now updates as expected.
Set Mode=OneWay in Binding
Method 1: In Xaml
<ListView ItemsSource="{x:Bind things, Mode=OneWay}" />
Method 2: In Code Behind
Binding myBind = new Binding();
myBind.Source = things;
myBind.Mode = BindingMode.OneWay;
myListView.SetBinding(ListView.ItemsSourceProperty, myBind);
It is a bug in IRealmCollection. You can use Prerelease Nuget to solve it.
For more info:
IRealmCollection does not update UWP ListView
GitHub Issue: IRealmCollection does not update UWP ListView

Can I access XAML elements in an array in the codebehind?

I've been looking around but I haven't been able to find anything on this. I am trying to get started making Windows 8.1 apps in C# with Visual Studio 2013 Pro. I want to be able to access multiple elements (particularly buttons or text blocks) in an array because this is more convenient for developing things like board games. For instance, if I were developing tic-tac-toe, I might use a series of buttons like this:
<Grid>
<Button Name="Cell00"/>
<Button Name="Cell01"/>
<Button Name="Cell02"/>
<Button Name="Cell10"/>
<Button Name="Cell11"/>
<Button Name="Cell12"/>
<Button Name="Cell20"/>
<Button Name="Cell21"/>
<Button Name="Cell22"/>
<Grid/>
Now for the function that would check for a win, I would have to check all possible combinations like this is in the code behind:
private bool CheckForWin()
{
if((Cell00 == Cell01) && (Cell01 == Cell02) && isNotBlank(Cell02)) return true;
if((Cell10 == Cell11) && (Cell11 == Cell12) && isNotBlank(Cell12)) return true
...
return false; //if none of the win conditions pass
}
This type of code would be extremely cumbersome. I would like to write it instead in a way that lets me check the array with for loops.
I realize that with tic-tac-toe, it is fairly easy to code it using brute force, but this was the first example that came to my head. Other games like Reversi or Go would not work well like this because of either the sheer size or the fact that pieces placed can change other cells than the one they were placed on.
Any help with this would be greatly appreciated.
This is not the correct way to use WPF. WPF is designed to use data binding....creating and manipulating UI elements directly is bad form. There are more posts/discussion/questions about this than you can imagine and I'll leave you to research them for yourself. In the mean time this is how you use WPF "properly":
First use NuGet to add MVVM lite to your project so that you get the ViewModelBase class and create a view model for a single cell:
public class Cell : ViewModelBase
{
private string _Text;
public string Text
{
get { return _Text; }
set { _Text = value; RaisePropertyChanged(() => this.Text); }
}
}
One level up you'll want a main model to encapsulate an array of these, this is where you will typically do all your game logic:
public class MainModel : ViewModelBase
{
private ObservableCollection<Cell> _Cells;
public ObservableCollection<Cell> Cells
{
get { return _Cells; }
set { _Cells = value; RaisePropertyChanged(() => this.Cells); }
}
public MainModel()
{
this.Cells = new ObservableCollection<Cell>(
Enumerable.Range(1, 100)
.Select(i => new Cell { Text = i.ToString() })
);
}
}
Notice that all I'm doing at the moment is creating a 100-element collection of cells. This main view model becomes the one that you assign to your window's data context:
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainModel();
}
Now your XAML controls need to bind to this data. ItemsControl is used to render a collection of elements so use one of those and bind it to your array. You want them displayed in a 2D grid so replace the ItemsPanelTemplate with a WrapPanel. Finally add a DataTemplate for your Cell class so that a button gets drawn for each cell:
<Window.Resources>
<DataTemplate DataType="{x:Type local:Cell}">
<Button Width="32" Height="32" Content="{Binding Text}"/>
</DataTemplate>
</Window.Resources>
<ItemsControl ItemsSource="{Binding Cells}" Width="320" Height="320" HorizontalAlignment="Center" VerticalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
That's how you use WPF. Your logic is stored entirely in the view model and it's completely decoupled from the view. Here's what this particular code displays, it should be pretty self-evident how flexible this code is and easy to change:
That's very possible. Simply declare an array variable :
private Button[] _buttonArray;
populate the array once, maybe in constructor :
_buttonArray = new[] {Cell00, Cell01, .... , Cell22};
And all of the buttons are now accessible through _buttonArray.

WPF Binding Issue - UI Updates, Object Does Not

I'm having yet another WPF binding issue. Just when I think I've got this stuff figured out, I run into more problems... :S
Anyway... I've created a custom user control for selecting files. It's a simple textbox followed by a button contained within a grid. The property of the control with which I am working is called FilePath and the TextBox on this control is bound to that property. When the button is clicked, a SaveFileDialog is opened and the user selects a file. The UI correctly updates after the user selects the file.
The problem I seem to be having is that when I bind an object to the control (in this instance I have an object with a DocumentFilePath property) the object doesn't update when a new file is selected.
Here's the relevant code within my user control:
public static readonly DependencyProperty FilePathProperty = DependencyProperty.Register("FilePath", typeof(string), typeof(FileSave), new UIPropertyMetadata(string.Empty, OnFilePathChanged));
public string FilePath
{
get
{
return this.GetValue(FilePathProperty) as string;
}
set
{
this.SetValue(FilePathProperty, value);
this.OnPropertyChanged("FilePath");
}
}
private void OnPropertyChanged(string propName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
private static void OnFilePathChanged(object sender, DependencyPropertyChangedEventArgs e)
{
((FileSave)sender).OnPropertyChanged("FilePath");
}
And the user control is added into my Window programatically by using reflection on my object:
private void AddFileSave(PropertyInfo pi)
{
FileSave fs = new FileSave();
Binding b = new Binding(pi.Name);
fs.SetBinding(FileSave.FilePathProperty, b);
this.AddToGrid(fs); //adds the control into my window's grid in the correct row and column; nothing fancy here
}
It may be worth noting that if I load the window with an existing object, my user control displays properly but still won't register any changes within the object to which it is bound.
Please let me know if you guys need any more info.
Thanks in advance,
Sonny
EDIT: I've found a way around the problem, but this probably isn't a good solution. By watching the debugger carefully I found that when I set the FilePath property within my control, the object was being unbound. If anyone can shed some light on that, I would be most appreciative. In the mean time, I've changed the code that opens my SaveFileDialog to look like this:
private void Button_Click(object sender, RoutedEventArgs e)
{
Microsoft.Win32.OpenFileDialog ofd = new Microsoft.Win32.OpenFileDialog();
ofd.Multiselect = false;
ofd.Title = "Select document to import...";
ofd.ValidateNames = true;
ofd.ShowDialog();
if (this.GetBindingExpression(FilePathProperty) == null)
{
this.FilePath = ofd.FileName;
}
else //set value on bound object (THIS IS THE NEW PORTION I JUST ADDED)
{
BindingExpression be = this.GetBindingExpression(FilePathProperty);
string propName = be.ParentBinding.Path.Path;
object entity = be.DataItem;
System.Reflection.PropertyInfo pi = entity.GetType().GetProperty(propName);
pi.SetValue(entity, ofd.FileName, null);
}
if (!string.IsNullOrWhiteSpace(this.FilePath))
{
_fileContents = new MemoryStream();
using (StreamReader sr = new StreamReader(this.FilePath))
{
_fileContents = new MemoryStream(System.Text.ASCIIEncoding.ASCII.GetBytes(sr.ReadToEnd()));
}
}
else
{
_fileContents = null;
}
}
You're not specifying anywhere in your code that the FilePath property should be TwoWay so updates of the DP value won't get pushed to the bound source object's property. You can use either:
Binding b = new Binding(pi.Name){ Mode = BindingMode.TwoWay };
or you can set up your Dependency Property to use a default of TwoWay:
public static readonly DependencyProperty FilePathProperty = DependencyProperty.Register(
"FilePath", typeof(string), typeof(FileSave),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnFilePathChanged));
You should also follow Robert's suggestion of removing the manual PropertyChange event, and also don't EVER add any code other than GetValue and SetValue in your DP wrapper property. XAML calls GetValue and SetValue directly so will skip over anything else you add there - which can lead to very nasty bugs.
Why, yes! I most certainly can shed some light on that!
Also, if you're using .Net 4.0, today's your lucky day!
Consider the following fine method on your DependencyObject:
SetCurrentValue();
Yes! With this SINGULAR method, all your woes will drift away as a bad dream at the rooster's crow! (Well, ok, not really, but that is the method you're looking for.)
Short story very short: When you programmatically SetValue() on a control in your view layer, you blow away your bindings. SetCurrentValue() was added to the framework because you frequently want to drive a change in your bound object by setting that value directly. An alternate design would be to set the value in your bound object programmatically and let the updated value get pulled back into the view, but that's frequently clumsy.
(I strongly suspect that the absence of this method up to this point is largely responsible for the utter failure of the vast majority of NumericUpDown controls in WPF.)
First, you don't need to raise the PropertyChanged event when a dependency property changes; with dependency properties, change notification comes for free.
What's probably happening here: The default behavior for UpdateSourceTrigger is LostFocus, i.e. the source gets updated when the user presses TAB to move to the next field, or clicks on another control, or whatever. The text box isn't losing focus after your SaveFileDialog sets Text (since it probably doesn't even have the focus in the first place), so the source update never gets triggered.
To make it update the source whenever the Text property changes, set the UpdateSourceTrigger to PropertyChanged.
If that doesn't work, watch the Output window for binding errors.
Edit:
Here's a little prototype application I built. It works just fine: typing in the text box sets the property, clicking on the "Save" button sets the property, and the binding in the main window gets updated properly no matter what.
<Window x:Class="DependencyPropertyBindingDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:demo="clr-namespace:DependencyPropertyBindingDemo"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<demo:FilePicker x:Name="Picker"
DockPanel.Dock="Top"
Margin="5" />
<TextBox DockPanel.Dock="Top"
Text="{Binding ElementName=Picker, Path=FilePath}" />
<TextBlock />
</DockPanel>
</Window>
<UserControl x:Class="DependencyPropertyBindingDemo.FilePicker"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DockPanel>
<TextBox DockPanel.Dock="Left"
Width="200"
Text="{Binding FilePath, UpdateSourceTrigger=PropertyChanged}" />
<Button Width="50"
DockPanel.Dock="Left"
Command="{Binding Path=SaveCommand}">Save</Button>
<TextBlock />
</DockPanel>
</UserControl>
public partial class FilePicker : UserControl
{
public FilePicker()
{
SaveCommand = new FilePickerSaveCommand(this);
DataContext = this;
InitializeComponent();
}
public ICommand SaveCommand { get; set; }
public static readonly DependencyProperty FilePathProperty = DependencyProperty.Register("FilePath", typeof(string), typeof(FilePicker));
public string FilePath
{
get
{
return GetValue(FilePathProperty) as string;
}
set
{
SetValue(FilePathProperty, value);
}
}
}
public class FilePickerSaveCommand : ICommand
{
private FilePicker _FilePicker;
public FilePickerSaveCommand(FilePicker picker)
{
_FilePicker = picker;
}
public void Execute(object parameter)
{
_FilePicker.FilePath = "Testing";
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}

Categories