WPF: Data binding to common property across views - wpf

I have a WPF application, where I want to keep a centralized date selection. I want to allow the date to set through one screen, and update it on others. Below is the common service,
public interface IDateService
{
public DateTime ScheduledDate { get; set; }
}
public sealed class DateService : ObservableObject, IDateService
{
private DateTime _scheduledDate = DateTime.Now.AddDays(1);
public DateTime ScheduledDate
{
get => _scheduledDate;
set
{
SetProperty(ref _scheduledDate, value);
}
}
}
I inject this though the constrictor of the view models of each screen.
public DateSetViewModel( IDateService dateService, IDialogCoordinator dialogCoordinator)
{
_dateService = dateService;
}
public DateTime ScheduledDate
{
get => _dateService.ScheduledDate;
set
{
_dateService.ScheduledDate = value;
}
}
and on read only views
public class DateReadViewModel : ObservableObject
{
private readonly IDateService _dateService;
public DateReadViewModel( IDateService dateService, IDialogCoordinator dialogCoordinator)
{
_dateService = dateService;
}
public DateTime ScheduledDate
{
get => _dateService.ScheduledDate;
}
...
}
Now, when loading, all screen shows initial date (now +1 day). Any update made through DateSetViewModel is reflected on that page UI. But, when switch to other views, it always shows initial date, not the updated value from IDateService. I tried to directly bind to dateService.ScheduledDate in other views, but it didn't work. I use MahApps.Metro to define the views if that matters.
The bindings on DateSetView
<DatePicker Width="100"
Margin="{StaticResource ControlMargin}"
SelectedDate="{Binding ScheduledDate}" />
and other views, I tried few, but similar to
<DatePicker Width="100"
Margin="5"
mah:TextBoxHelper.AutoWatermark="True"
SelectedDate="{Binding ScheduledDate, Mode=OneWay}" />
<TextBlock
Margin="5"
VerticalAlignment="Center"
Text="{Binding ScheduledDate}"
/>

DateService is the only object that raises the PropertyChanged event when its ScheduledDate property is set.
Given your current implementation, you need to subscribe to this event in the view models and raise the event for each data-bound source property:
public class DateReadViewModel : ObservableObject
{
private readonly IDateService _dateService;
public DateReadViewModel(IDateService dateService, IDialogCoordinator dialogCoordinator)
{
_dateService = dateService;
_dateService.PropertyChanged += OnDateServicePropertyChanged;
}
private void OnDateServicePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(IDateService.ScheduledDate))
OnPropertyChanged(nameof(ScheduledDate));
}
public DateTime ScheduledDate
{
get => _dateService.ScheduledDate;
}
...
}
public interface IDateService : INotifyPropertyChanged
{
public DateTime ScheduledDate { get; set; }
}
The other option is to bind directly to the property of the service.

They all work initially because the binding will fetch the value at the time the views are instantiated. Binding dependency properties (like the Text of the TextBlock and the SelectedDate of the DatePicker) though requires some sort of notification so the Binding(Expression) knows to refetch the (updated) value from the source. While your DateService may be sending a notification, those objects you are exposing the DateTime from (and which is the source for the binding) are not (e.g. the DateReadViewModel or DateSetViewModel). You should make those classes provide a notification using one of the mechanisms documented - see the Binding Sources and Targets section of this page for a description of those mechanisms. Basically make ScheduledDate a dependency property, implement a ScheduledDateChanged event -or- implement INotifyPropertyChanged. Or I suppose you could expose the Service itself from the view model but that to me is exposing an internal implementation detail and should be avoided.

Related

AutoGenerateField not working in wpf datagrid

I am binding my datagrid to an observableCollection of Class CustomerDetails, I want to hide one of the property from being displayed on the UI as column, for which i am using AutoGenerateField to false, still this column is getting displayed in UI, what am i missing ?
my xaml file is like below :
<DataGrid AutoGenerateColumns="True" ItemsSource="{Binding DataGridItems}"
Margin="1" IsReadOnly="True" SelectedIndex="{Binding SelectedItem }"
ViewModel.cs:
public ObservableCollection<CustomerDetails> DataGridItems => _model.CustomerDetailsList;
Model.cs
public ObservableCollection<CustomerDetails> CustomerDetailsList { get; set; }
public MyModel()
{
CustomerDetailsList = new ObservableCollection<CustomerDetails>(); // assume that my list of customers is initialized here
}
public class CustomerDetails
{
#region Constructor
public CustomerDetails()
{
}
#endregion
#region Public Members
public string CustomerName
{
get; set;
}
public string CustomerID
{
get;set;
}
public string ProductCode
{
get;set;
}
// want to hide this from getting shown in Datagrid
[Display(AutoGenerateField = false)]
public string ProductInternalId { get; set; }
The DataGrid in WPF doesn't check whether a property is decorated with the DisplayProperty so decorating your property with this attribute will have no effect.
What you should do is to either set the AutoGenerateColumns property to false and explicitly define the columns you want in your XAML markup, or handle the AutoGeneratingColumn event:
private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
e.Cancel = e.PropertyName == "ProductInternalId ";
}
Likely you are running into the same underlying problem as:
Display "Display name" instead of field name in WPF DataGrid
You should handle the AutoGeneratingColumn event to cancel the generation of the column (you can base your logic on the field attributes to still make use of [Display(AutoGenerateField=false)]):
https://learn.microsoft.com/en-us/windows/communitytoolkit/controls/datagrid_guidance/customize_autogenerated_columns
Alternatively you could turn off AutoGenerateColumns and add the ones you want to the datagrid columns template manually.

IDataErrorInfo does not fire when binding to a complex object

I have a simple dialog that contains edit boxes such as this:
<TextBox Text="{Binding Path=EmailSettings.SmtpServer, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}" />
The dialog uses a Model as its data context (to simplify the model example INotifyPropertyChanged has not been shown nor is the code that creates the model and
sets the dialog data context to the model instance):
class EmailSettingsModel : IDataErrorInfo
{
public EmailSettingsModel ()
{
EmailSettings = new EmailSettings();
}
public EmailSettings EmailSettings
{ get; set; }
string _error;
public string Error
{
get { return _error; }
set { _error = value; }
}
public string this[string propertyName]
{
get
{
string errorMessage = null;
if ( string.Compare( propertyName, "EmailSettings.SmtpServer" ) == 0 )
{
if ( !string.IsNullOrWhiteSpace( EmailSettings.SmtpServer ) )
errorMessage = "SMTP server is not valid";
}
Error = errorMessage;
}
}
}
The model contains a property that is a simple POCO class that has several properties on it.
class EmailSettings
{
public string SmtpServer
{ get; set; }
}
I could not get the IDataErrorInfo indexer to fire and spent hours looking. When I changed the binding on the text box to use a simple property:
<TextBox Text="{Binding Path=SmtpServer, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}" />
on the Model as below the IDataErrorInfo indexer fired.
class EmailSettingsModel
{
public string SmtpServer
{ get; set; }
}
Was IDataErrorInfo not called because I used a compound property for the binding statement. I have used complex properties like this for normal data binding and they work but for this example IDataErrorInfo was not called.
IDataErrorInfo fires only at the level where implemented
For example if you have Binding Path looking like this "viewModel.property1.property2.property3" you will need to implement IDataErrorInfo inside the class of viewModel and inside the class of property1 and inside the class of property2. Property3 is a string.
So in order to make it work for you just implement IDataErrorInfo anywhere else.

How do I bind DateTime as DepedencyProperty of a DependencyObject so that it will show up in a textbox?

I want to learn how to use Dependency Objects and Properties. I have created this class,
public class TestDependency : DependencyObject
{
public static readonly DependencyProperty TestDateTimeProperty =
DependencyProperty.Register("TestDateTime",
typeof(DateTime),
typeof(TestDependency),
new PropertyMetadata(DateTime.Now));
public DateTime TestDateTime
{
get { return (DateTime) GetValue(TestDateTimeProperty); }
set { SetValue(TestDateTimeProperty, value); }
}
}
The window class is like this
public partial class MainWindow : Window
{
private TestDependency td;
public MainWindow()
{
InitializeComponent();
td = new TestDependency();
td.TestDateTime = DateTime.Now;
}
}
Now I want to use it to show a the current DateTime in the TextBlock which updates itself every second, by adding this to a grid
<Grid>
<TextBlock Text="{Binding TestDateTime,ElementName=td}" Width="200" Height="200"/>
</Grid>
I can see the TextBlock, but there is no Date Time value in it at all. What am I doing wrong?
First of all if you want to update the display time once a second your going to need a timer to trigger an update. A DispatchTimer works works well for that.
public class TestDependency : DependencyObject
{
public static readonly DependencyProperty TestDateTimeProperty =
DependencyProperty.Register("TestDateTime", typeof(DateTime), typeof(TestDependency),
new PropertyMetadata(DateTime.Now));
DispatcherTimer timer;
public TestDependency()
{
timer = new DispatcherTimer(new TimeSpan(0,0,1), DispatcherPriority.DataBind, new EventHandler(Callback), Application.Current.Dispatcher);
timer.Start();
}
public DateTime TestDateTime
{
get { return (DateTime)GetValue(TestDateTimeProperty); }
set { SetValue(TestDateTimeProperty, value); }
}
private void Callback(object ignore, EventArgs ex)
{
TestDateTime = DateTime.Now;
}
}
Next we need to modify the XAML so it binds properly to the updated dependency object.
<Window.DataContext>
<local:TestDependency/>
</Window.DataContext>
<Grid>
<TextBlock Text="{Binding TestDateTime}" />
</Grid>
Since we set the DataContext in XAML you can actually delete all of the code behind code in the MainWindow constructor.
If you just want to show some values in your TextBlock, you don't need a Dependency Object here. Try something like this:
public partial class MainWindow : Window
{
public DateTime Test
{ get; set; }
public MainWindow()
{
InitializeComponent();
this.Test = DateTime.Now;
}
}
<Grid>
<TextBlock Text="{Binding Path=Test,RelativeSource={RelativeSource AncestorType=Window,Mode=FindAncestor}}"></TextBlock>
</Grid>
Here I am not showing the code which can update the value every second. I just want to clarify that this is not the right situation to use Dependency Property.
Of course you can use Dependency Property to do this. But Dependency Object and Dependency Property can offer you some extension functionality such as Data Binding. But it doesn't mean that you need to use a Dependency Object or Dependency Property as the source of the Data Binding.

Caliburn.Micro: Communication between user controls

I'm building my first Caliburn WPF application, and I find myself in the following problem.
I have a parent view, with loads two user controls: Search & Results. When the search button is clicked on the Search user control, I wan't to load the results in the results user control.
Parent View:
<ContentControl x:Name="SearchViewModel"/>
<ContentControl x:Name="ResultsViewModel"/>
Parent VM
[Export(typeof(IMainViewModel))]
public class ParentViewModel : Screen, IMainViewModel{
public SearchViewModel SearchViewModel { get; set; }
public ResultsViewModel ResultsViewModel { get; set; }
public ParentViewModel()
{
SearchViewModel = new SearchViewModel();
ResultsViewModel = new ResultsViewModel();
}
}
Search View
<TextBox x:Name="Term"/>
<Button Content="Search" x:Name="Search"/>
Search VM
public class SearchViewModel : PropertyChangedBase
{
private string _term;
public string Term
{
get { return _term; }
set
{
_instrumentId = value;
NotifyOfPropertyChange(() => _term);
}
}
public void Search()
{
//Call WCF Service
//Send results to results user control?
}
}
So actually how can i pass or access data/methods between different user controls / view models with caliburn micro?
You can use events via the Caliburn Micro Event Aggregator. You can publish an event in one viewmodel and subscribe that event in the other. This keep the model decoupled - the only coupling is done by the event itself-, in which you can store the data to transfer.

Two-way bind a combobox to a simple string array

I have a simple class that provides state codes like this:
public class StateProvider
{
static string[] _codes = new string[]
{
"AL",
"AK",
...
};
public string[] GetAll()
{
return _codes;
}
}
My model class that supports the view looks a little like this:
public class ProjectModel : ChangeNotifier
{
StateProvider _states = new StateProvider();
public ProjectModel()
{
Project = LoadProject();
}
ProjectEntity _project;
public ProjectEntity Project
{
get { return _project; }
set
{
_project = value;
FirePropertyChanged("Project");
}
}
public string[] States { get { return _states.GetAll(); } }
}
And my ComboBox XAML looks like this:
<ComboBox SelectedValue="{Binding Project.State, Mode=TwoWay}" SelectedValuePath="{Binding RelativeSource={RelativeSource Self}}" ItemsSource="{Binding States}" />
The binding works from the UI to the entity - if I select the state from the combo then the value gets pushed to the project entity and I can save it. However, if I shutdown and reload, the state code value doesn't bind from the entity to the UI and the combo shows nothing selected. Then, of course, a subsequent save nulls the entity's state value.
I want this very simple since I want to display state codes and save state codes (I don't want to display the full state name). So I don't want to have to muck with creating a State class that has Code and FullName properties and avoid having to use the SelectedValuePath and DisplayMemberPath properties of the combobox.
Edit:
Added to the code how ProjectModel does change notification. Note that the ProjectEntity class does this too. Trust me, it works. I've left it out because it also inherits from an Entity base class that does change notification through reflection. TwoWay binding works on everything but for the combobox.
You have to at least implement IPropertyNotifyChanged on your ProjectModel class
public class ProjectModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
and implement the Project property as below for binding to work other than 1-way-1-time.
public ProjectEntity Project
{
get { return (ProjectEntity)GetValue(ProjectProperty); }
set { SetValue(ProjectProperty, value); }
}
// Using a DependencyProperty as the backing store for Project.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty ProjectProperty =
DependencyProperty.Register("Project",
typeof(ProjectEntity),
typeof(ProjectModel),
new PropertyMetadata(null,
new PropertyChangedCallback(OnProjectChanged)));
static void OnProjectChanged(object sender, DependencyPropertyChangedEventArgs args)
{
// If you need to handle changes
}
Wow, whodathought it'd come down to this:
<ComboBox ItemsSource="{Binding States}" SelectedValue="{Binding Project.State, Mode=TwoWay}" />
It turned out it was the order in which I placed the attributes in the XAML. The SelectedValue binding was happening before the ItemsSource binding and thus there were no items in the combobox when the SelectedValue was bound.
Wow, this just seems like a really bad thing.
Change your ProjectModel class to this:
public class ProjectModel : ChangeNotifier
{
StateProvider _states = new StateProvider();
public ProjectModel()
{
Project = LoadProject();
States = new ObservableCollection<string>(_states.GetAll());
}
ProjectEntity _project;
public ProjectEntity Project
{
get { return _project; }
set
{
_project = value;
FirePropertyChanged("Project");
}
}
public ObservableCollection<string> States { get; set; }
}
Also make sure that ProjectEntity also implements INotifyPropertyChanged.

Resources