I'm struggling with WinForms data binding. The issue is that the controls should be bound to a model but they just throw ArgumentException without any reasonable reason. It's thrown exactly when a TabPage shows the control in question. This is the following Exception.Message: Unable to link property or Mpn column in DataSource.
So, I can't figure out what's going wrong. It seems to be ok.
It's worth to mention that I'm implementing a "forcing" MVVM to WinForms and the pattern was working fine until now (lol!). I already know that MVVM goes better with WPF and Silverlight, so please, focus on the issue.
I leave the code below and some explanation about how they are composed:
View: It has a TabControl with many tabs page inside and those TabPage's have controls such as: Labels, TextBox, ComboBox and so on.
public sealed partial class ProductView : ViewBase
public ProductView(ProductViewModel viewModel) : base(viewModel) => InitializeComponent();
public sealed async override void BindViewModel()
{
ProductViewModel pvm = ViewModel as ProductViewModel;
await Task.Run(() => { while (pvm.Loading) ; });
DataSourceUpdateMode updateMode = DataSourceUpdateMode.OnPropertyChanged;
// Product Tab Data
materialTextField_mpn.DataBindings.Add("Text", pvm, nameof(pvm.Product.Mpn), false, updateMode);
}
private void ProductView_Load(object sender, EventArgs e){
(ViewModel as ProductViewModel).Load.Execute(null);
BindViewModel();
}
}
ViewModel
public sealed class ProductViewModel : ViewModelBase
{
private bool loading;
private int product_id;
private readonly IRepository<ProductModel> repository;
private ICommand load;
public ProductViewModel(IRepository<ProductModel> repository) => this.repository = repository;
public ProductModel Product { get; private set; }
private async void LoadData()
{
Loading = true;
product_id = 0; // Hardcoded for testing purposes.
Product = product_id > 0 ? await repository.GetByIdAsync(product_id) : new ProductModel()
{
ID = 0,
Mpn = string.Empty
};
Loading = false;
}
Model
public class ProductModel : ModelBase
{
private string mpn;
public string Mpn {
get => mpn;
set
{
if (mpn == value)
return;
mpn = value;
OnPropertyChanged(nameof(mpn));
}
}
}
Many thanks in advance!
UPDATE & SOLUTION
After sleeping for a while and coming back to code. I realized that the Data Binding was wrong. Excatly this line:
materialTextField_mpn.DataBindings.Add("Text", pvm, nameof(pvm.Product.Mpn), false, updateMode);
Just change for this one:
materialTextField_mpn.DataBindings.Add("Text", pvm.Product, nameof(pvm.Product.Mpn), false, updateMode);
Don't be a dummy like me lol!
Related
I am writing a program using Xamarin, Shell and MVVM.I want to send a parameter to next page and I am using the following code:
await Shell.Current.GoToAsync($"//{nameof(CopyBooksPage)}?RegisteredUserId={registeredUser.Id}");
Question: How to get the parameter before binding?
First wrong solution:
In CopyBooksPage.xaml I have binding to properties in VM but I have no affiliation with the VM.
I do this:
[XamlCompilation(XamlCompilationOptions.Compile)]
[QueryProperty(nameof(RegisteredUserId), "RegisteredUserId")]
public partial class CopyBooksPage : ContentPage
{
private int _registeredUserId;
public int RegisteredUserId
{
get { return _registeredUserId; }
set
{
_registeredUserId = value;
CopyBooksViewModel copyBooksViewModel = App.GetViewModel<CopyBooksViewModel>();
copyBooksViewModel.RegisteredUserId = _registeredUserId;
BindingContext = copyBooksViewModel;
copyBooksViewModel.RefreshBinding();
}
}
public CopyBooksPage()
{
InitializeComponent();
}
}
Call order:
construktor
properties RegisteredUserId
The problem is twofold.
After calling the constructor, I get information about binding errors (because there is no VM). It's not annoying but I want (need) to get rid of it.
I do the binding only in the propertis and this causes the problem that for each VM propertis I have to call the OnPropertyChanged method to refresh the binding. And this is troublesome for me. I do not want to do it.
Second wrong solution
Code behind:
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CopyBooksPage : ContentPage
{
public CopyBooksPage()
{
InitializeComponent();
BindingContext = App.GetViewModel<CopyBooksViewModel>();
}
}
ViewModel:
class CopyBooksViewModel : BaseViewModel, IQueryAttributable
{
private int registeredUserId;
//read from database
private CopyBook copyBookModel;
public string BookTitle
{
get { return copyBookModel.Title; }
set
{
copyBookModel.Title = value;
OnPropertyChanged(nameof(Title));
}
}
public CopyBooksViewModel()
{
}
public void ApplyQueryAttributes(IDictionary<string, string> query)
{
if (query.ContainsKey("RegisteredUserId"))
{
registeredUserId = int.Parse(HttpUtility.UrlDecode(query["RegisteredUserId"]));
copyBookModel = ReadFromDatabase(registeredUserId);
}
}
}
Call order:
construktor
Binding
method ApplyQueryAttributes
When binding the BookTitle, I reference the copyBookModel which is null. I could secure it.
The problem is that the ApplyQueryAttributes method is called last. In it again I would have to call OnPropertyChanged for all propertis. I do not want to do it.
I am struggling with Text binding in my WPF app.
Lets imagine that I have another working app (ex. windows service) with some data in it.
In my WPF app I would like to have folder "DATA" with class where data are introduced and in same folder another class which would include a void which will query my windows service
I would like to show this data in my WPF window.
To make it simpler - one class with data, one class with data changing and WPF window with showing this data.
Unfortunately I can not achieve this... When I am executing below code, my window is showing 0 instead 123.
I would like to achive that my window will show value 123.
file "Database.cs" in folder "Data" in project "example"
namespace example.Data
{
public class Database
{
private int _testInt = 0;
public int testInt
{
get { return _testInt; }
set { _testInt = value; }
}
}
}
file "Query.cs" in folder "Data" in project "example"
namespace example.Data
{
public class Query
{
public Database _database;
public void execute()
{
_database = new Database();
_database.testInt = 123;
}
}
}
file "MainWindow.xaml.cs" in project "example"
namespace example
{
public partial class MainWindow : Window
{
public Data.Database _database;
public Data.Query _query;
public int testInt
{
get { return _database.testInt; }
set { _database.testInt = value; OnPropertyChanged(); }
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
_database = new Data.Database();
_query = new Data.Query();
_query.execute();
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
#endregion
}
}
File MainWindow.xaml
<Window>
<TextBlock Text="{Binding testInt}"
Foreground="White"
FontSize="15"
VerticalAlignment="Top"
HorizontalAlignment="Left"
Margin="20,10,10,0" />
</Window>
P.S. If I will put
_database.testInt = 987;
to MainWindow.xaml.cs it is working properly - window is showing value 987 in textblock.
You have multiple instances of the Database object, a new one each time Query.execute is called and one in MainWindow constructor.
It's the data in the later that is displayed.
You should modify the content of this instance to see any change, for that, you must inject it in the Query object:
_query = new Data.Query(_database);
// ...
public class Query
{
private readonly Database _database;
public Query(Database database)
{
_database = database;
}
public void Execute()
{
_database.testInt = 123;
}
}
Finally you need a way to notify the view that the content as changed, that why Database should implement INotifyPropertyChanged.
But at this point it's badly named, because it's a model in the MVVM pattern.
you need to implement INotifyPropertyChanged
public partial class MainWindow : Window, INotifyPropertyChanged
from the MVVM view, I think these answers from Orace and Jason are on a good way, both do not solve the problem completely.
Let the Mainwindow implement INotifyPropertyChanged
Let the query accept the new value:
public void execute(int value)
{
//_database = new Database();
// inject _database like in the answer above
_database.testInt = value;
}
When your testInt changes, let the _query deliver the change down to the "database" (btw: you do it vice versa) See code below:
`public int testInt
{get { return _database.testInt; }
`set { _query.execute(value); OnPropertyChanged(); }`
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
_database = new Data.Database();
// the property change will change both the view and the model
testInt = 987;
}
Well, you have changed both model and view with one property change then, Good or not?!
Just for future users. There is small bug in Orace's answer: (It should be without "readonly" parameter, because below You are writing to it.
private Database _database;
public Query(Database database)
{
_database = database;
}
I am trying to pass a value to a view model from another view model before navigating to the page attached to that view model.
I was previously passing it to the view, then passing it to the view model. This seems like a clumsy way of doing things.
I am not using any kind of framework so that is not an option.
At the moment the property is set as static and this works but im not sure if this is good practice.
The code:
View model 1:
This command opens the new page:
public void OpenRouteDetails()
{
RouteStopPopOverViewModel.RouteName = "TestRoute";
App.Page.Navigation.PushAsync(new RouteStopPopOverView());
}
View model 2: (RouteStopPopOverViewModel)
public static string RouteName { get; set; }
This does work but I would prefer not to use static as a way to achieve this.
Is there some way to set the RouteName property without using static or passing it through view-> view model.
I have seen some answers about this but they don't seem to answer to question clearly.
Share a controller class between view models.
The same instance has to be supplied to the constructor in both view models.
So you can set values, and listen for events in both view models.
The controller class becomes the intermediary.
public class SharedController : IControlSomething
{
private string _sharedValue;
public string SharedValue
{
get => _sharedValue;
set
{
if (_sharedValue == value)
return;
_sharedValue = value;
OnSharedValueUpdated();
}
}
public event EventHandler SharedValueUpdated;
protected virtual void OnSharedValueUpdated()
{
SharedValueUpdated?.Invoke(this, EventArgs.Empty);
}
}
public class ViewModel1
{
private readonly IControlSomething _controller;
public ViewModel1(IControlSomething controller)
{
// Save to access controller values in commands
_controller = controller;
_controller.SharedValueUpdated += (sender, args) =>
{
// Handle value update event
};
}
}
public class ViewModel2
{
private readonly IControlSomething _controller;
public ViewModel2(IControlSomething controller)
{
// Save to access controller values in commands
_controller = controller;
_controller.SharedValueUpdated += (sender, args) =>
{
// Handle value update event
};
}
}
here the sample you can achieve your requirement easily with navigation
public class ViewModelFrom : BaseViewModel
{
async Task ExecuteCommand()
{
string routeName="value to trasfer";
Navigation.PushAsync(new View(routeName));
}
}
public partial class View : ContentPage
{
public View(string routeName)
{
InitializeComponent();
BindingContext = new ViewModelTo(routeName);
}
}
public class ViewModelTo : BaseViewModel
{
public string RouteName { get; set; }
public ViewModelTo(string routeName)
{
RouteName=routeName;
}
}
If there is a hierarchy you could express that in a parent to both of them.
public class Route
{
private string Name;
}
public class RouteSelectedArgs : EventArgs
{
public Route Selected { get; set; }
}
public interface IRouteSelection
{
event EventHandler<RouteSelectedArgs> RouteSelected;
}
public interface IRouteDetails { }
public class RouteWizard
{
public UserControl view { get; set; }
private IRouteSelection _selection;
private IRouteDetails _details;
public RouteWizard(IRouteSelection selection, IRouteDetails details)
{
_selection = selection;
_details = details;
_selection.RouteSelected += Selection_RouteSelected;
view = MakeView(_selection);
}
private void Selection_RouteSelected(object sender, RouteSelectedArgs e)
{
_selection.RouteSelected -= Selection_RouteSelected;
view = MakeView(_details, e.Selected);
}
private UserControl MakeView(params object[] args)
{
////magic
throw new NotImplementedException();
}
}
As you are using the MVVM pattern, you can use one of the many MVVM Frameworks to achieve this.
I use FreshMvvm and it allow me to pass parameters between view models like this
await CoreMethods.PushPageModel<SecondPageModel>(myParameter, false);
Then in SecondPageModel I can see access the parameters in the Init method
private MyParamType _myParameter;
public override void Init(object initData)
{
base.Init(initData);
var param = initData as MyParamType;
if (param != null)
{
_myParameter = param;
}
}
You can find more details about FreshMvvm here although most MVVM frameworks have similar functionality.
I am working in Visual Studio running a Windows application.
I am wondering if I can fill a DataGridView from a TextBox, that was a passed value itself?
For example, the user would search for a patient from a dialog form. The patient's name they select would populate a TextBox on my main form. I want that selected patients prior test history to populate a DataGridView on that main form within a tab.
Is this possible, if so how would I accomplish this?
It is possible. I would suggest setting up some sort of data binding. More specifically you will want some class that maintains state and data binds to your controls and possibly your dialog form. I don't know how much you are looking for so this might be going overboard but I would suggest something like this:
public class MainForm : Form
{
public MainForm(StateManager stateManager)
{
_stateManager = stateManager;
//data binding for your text box
txtPatientName.DataBindings.Add(nameof(txtPatientName.Text), stateManager, nameof(stateManager.PatientName));
//data binding for your grid
historyGrid.DataSource = stateManager.History;
}
private void btnShowForm_Click(object sender, EventArgs e)
{
using(var form = new DialogForm())
{
var result = form.ShowDialog();
if(result == DialogResult.Ok)
{
_stateManager.UpdatePatient(form.InputPatientName);
}
}
}
private StateManager _stateManager;
}
//this is the form where you enter the patient name
public class DialogForm : Form
{
//this holds the value where the patient's name is entered on the form
public string InputPatientName { get; set; }
}
//this class maintains your state
public class StateManager : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string PatientName
{
get { return _patientName; }
set
{
_patientName = value;
OnPropertyChanged(nameof(PatientName));
}
}
public BindingList<MedicalHistoryItems> History => _history ?? (_history = new BindingList<MedicalHistoryItems>());
public void UpdatePatient(string patientName)
{
History.Clear();
var historyRetriever = new HistoryRetriever();
History.AddRange(historyRetriever.RetrieveHistory(patientName));
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(propertyName);
}
private BindingList<MedicalHistoryItems> _history;
private string _patientName;
}
I have a bindingSource in winforms as well as a controller class.
I want to be able to set the selected record from within the controller class using 2 way binding.
That is If the form is displaying and I set the SelectedPerson in the controller then the bindingSOurce should make that person the current record.
My controller code is
public class PeopleController : BaseController
{
private SortableBindingList<Person> _blvPersons;
public SortableBindingList<Person> BlvPersons
{
get
{
return this._blvPersons;
}
set
{
this._blvPersons = value;
this.SendChange("BlvPersons");
}
}
private Person _selectedPerson;
public Person SelectedPerson
{
get
{
return this._selectedPerson;
}
set
{
this._selectedPerson = value;
this.SendChange("SelectedPerson");
this.SendChange("BlvPersons");
this.Trace("## SelectedPerson = {0}", value);
}
}
public void InitBindingList
{
using (var repo = new PeopleRepository(new OrganisationContext()))
{
IList<Person> lst = repo.GetList(p => p.Id > 0 && p.Archived == false, x => x.Organisation);
this.BlvPersons = new SortableBindingList<Person>(lst);
} }
}
//ect
}
public class BaseController : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;
public void SendChange(string propertyName)
{
System.Diagnostics.Debug.WriteLine("PropertyChanged {0} = {1}", propertyName, GetType().GetProperty(propertyName).GetValue(this, null));
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
// etc
I have a bindingSource on my form and set bindingSource.DataSource = controller.BlvPersons
If I Update data values using the controller I will see these changes in the form.
However I cant work out how to set the current record in the controller and see the change in the form.
You can use BindingSource.Find method and set the Position property to the results of the Find method.
The Find method can only be used when the underlying list is an
IBindingList with searching implemented. This method simply refers the
request to the underlying list's IBindingList.Find method.
To implement search on a generic BindingList requires various steps. First, you have to indicate that searching is supported by overriding the SupportsSearchingCore property. Next, you have to implement the IBindingList.Find method, which performs the search.
You can use examples from here or here.
Because I don't want a winforms reference in my controller class, I don't want to share the bindingSource between the form and the controller.
Instead I came up with the idea of having a RecordPosition property in the controller and binding it to a textbox
In my form I have
BindHelper.BindText(this.textRecordPosition,this.controller,"RecordPosition");
private void textRecordPosition_TextChanged(object sender, EventArgs e)
{
this.bindingSource.Position = Convert.ToInt32(textRecordPosition.Text) -1;
}
private void bindingSource_PositionChanged(object sender, EventArgs e)
{
this.controller.RecordPosition = this.bindingSource.Position + 1;
}
In my controller I have
public int RecordPosition
{
get
{
return this._position;
}
set
{
this._position = value;
this.SendChange("RecordPosition");
}
}
In my BindHelper class I have
public static void BindText(TextBox box, object dataSource, string dataMember)
{
var bind = new Binding("Text", dataSource, dataMember, true, DataSourceUpdateMode.OnPropertyChanged);
box.DataBindings.Add(bind);
}