The Dart programming language has support for method cascades. Method cascades would allow the following Silverlight/WPF C# code:
var listBox = new ListBox();
listBox.Width = 200;
listBox.MouseEnter += (s, e) => Console.WriteLine("MouseEnter");
var button1 = new Button() { Content = "abc" };
button1.Click += (s, e) => Console.WriteLine("button1.Click");
listBox.Items.Add(button1);
var button2 = new Button() { Content = "def" };
button2.Click += (s, e) => Console.WriteLine("button2.Click");
listBox.Items.Add(button2);
ContentPanel.Children.Add(listBox);
to be written instead as:
ContentPanel.Children.Add(
new ListBox()
..Width = 200
..MouseEnter += ((s, e) => Console.WriteLine("MouseEnter"))
..Items.Add(
new Button()
..Content = "abc";
..Click += ((s, e) => Console.WriteLine("button 1 Click")))
..Items.Add(
new Button()
..Content = "def";
..Click += (s, e) => (Console.WriteLine("button 2 Click"))));
My question is, is there a way to simulate or closely approximate method cascades in C#?
Here's one approach I came up with. Given this extension method:
public static T Call<T>(this T obj, Action<T> proc)
{
proc(obj);
return obj;
}
the above example can be written as follows:
ContentPanel.Children.Add(
new ListBox().Call(o => {
o.Width = 200;
o.MouseEnter += (s, e) => Console.WriteLine("MouseEnter");
o.Items.Add(
new Button().Call(b => {
b.Content = "abc";
b.Click += (s, e) => Console.WriteLine("button 1 Click"); }));
o.Items.Add(
new Button().Call(b => {
b.Content = "def";
b.Click += (s, e) => Console.WriteLine("button 2 Click"); })); }));
I wouldn't argue that that's pretty. :-) But it does essentially enable a fluent style to be applied.
I think you can reach close to what you want to achieve by using fluent interface. It will allow you to chain methods to create and initialize objects in one statement.
You can get something like that:
Fluent fluent = new Fluent();
var panel = fluent.CreateControlPanel().Children()
.AddListBox().SetWidth(200).AddMouseEnterEvent((s, e) => { }).Create()
.AddTextBox().SetText("Foo").Create()
.GetControlPanel();
The idea is that a method returns an object allowing to initialize another object. A chain of initializer can call at any item a "finalizer" method (above Create) that returns the original object (above Children) to continue to add other objects or configure the initial one.
So for example in AddListBox returns an object of type ListBoxSetup which has a bunch of methods like SetWidth or AddMouseEnterEvent. In this case Children will also be a special object (like of type ChildSetup) which has a bunch of methods such as AddListBox or AddTextBox. Each of the method has in charge to create an object of type ListBox or TextBox or setup properties of the underlying object to be created. Fluent will have a method that returns your whole object structure correctly setup.
Take a look to this link:
http://blog.raffaeu.com/archive/2010/06/26/how-to-write-fluent-interface-with-c-and-lambda.aspx
Here's an example of the underlying code create to end up with the above. Of course the code could be greatly improved in its architecture but it's here just for the sake of the example.
public class Fluent
{
public ControlPanelCreator CreateControlPanel()
{
return new ControlPanelCreator(new StackPanel(), this);
}
}
public class ControlPanelCreator
{
#region Fields
private Fluent fluent;
private Panel panel;
#endregion
#region Constructors
internal ControlPanelCreator(Panel panel, Fluent fluent)
{
this.fluent = fluent;
this.panel = panel;
}
#endregion
#region Methods
public ControlPanelChildrenCreator Children()
{
return new ControlPanelChildrenCreator(this.panel, this);
}
#endregion
}
public class ControlPanelChildrenCreator
{
#region Fields
private ControlPanelCreator panelCreator;
private Panel panel;
#endregion
#region Constructors
internal ControlPanelChildrenCreator(Panel panel, ControlPanelCreator panelCreator)
{
this.panel = panel;
this.panelCreator = panelCreator;
}
#endregion
#region Methods
public ListBoxCreator AddListBox()
{
ListBox listBox = new ListBox();
this.panel.Children.Add(listBox);
return new ListBoxCreator(listBox, this);
}
public TextBoxCreator AddTextBox()
{
TextBox textBox = new TextBox();
this.panel.Children.Add(textBox);
return new TextBoxCreator(textBox, this);
}
public Panel GetControlPanel()
{
return this.panel;
}
#endregion
}
public class ListBoxCreator
{
#region Fields
private ListBox listbox;
private ControlPanelChildrenCreator parentCreator;
#endregion
#region Constructors
internal ListBoxCreator(ListBox listBox, ControlPanelChildrenCreator parentCreator)
{
this.listbox = listBox;
this.parentCreator = parentCreator;
}
#endregion
#region Methods
public ListBoxCreator SetWidth(int width)
{
this.listbox.Width = width;
return this;
}
public ListBoxCreator AddMouseEnterEvent(Action<object, MouseEventArgs> action)
{
this.listbox.MouseEnter += new MouseEventHandler(action);
return this;
}
public ControlPanelChildrenCreator Create()
{
return this.parentCreator;
}
#endregion
}
public class TextBoxCreator
{
#region Fields
private TextBox textBox;
private ControlPanelChildrenCreator parentCreator;
#endregion
#region Constructors
internal TextBoxCreator(TextBox textBox, ControlPanelChildrenCreator parentCreator)
{
this.textBox = textBox;
this.parentCreator = parentCreator;
}
#endregion
#region Methods
public TextBoxCreator SetText(string defaultText)
{
this.textBox.Text = defaultText;
return this;
}
public ControlPanelChildrenCreator Create()
{
return this.parentCreator;
}
#endregion
}
I supported previous answer. Few things I like to add, as I have created similar type of things.
There are two things, either you are having single class doing things. Means Form, has addcolor, addData etc and may be form has button and than button has color
Now in this case you need to chain using interface, means method return type will be interface, and all interface is implemented by that class and that methods return "this" only.
This will do the trick, while you create object of interface. And then chain it across. It will be difficult to give example here, but if you still want I can provide example.
Let me know if any further details required
Related
I made a textbox that only accepts numbers and a "-" for negative numbers. I would like there to be an option to disable negative numbers.
In the constructor method I want to reference the allowNegatives bool that is defined properties editor and do different things depending on if it allows negatives values. I'm running into the problem that the 'allowNegatives' bool is always its default value in the constructor. If I reference it elsewhere it is the correct value.
Is there an way to get the assigned property value rather than the default value in the constructor?
public partial class ControlIntEntry : TextBox
{
private bool allowNegatives = false;
[Description("Allow negative values"), Category("Behavior")]
public bool AllowNegatives
{
get { return allowNegatives; }
set { allowNegatives = value; }
}
public ControlIntEntry()
{
// user sets AllowNegatives to true using properties editor
InitializeComponent();
Console.WriteLine(allowNegatives); // returns false
if (allowNegatives)
{
//do one thing
}
else
{
// do something else.
}
Task.Run(() => AfterConstructor()); // use for testing
}
private async Task AfterConstructor()
{
await Task.Delay(1000);
Console.WriteLine(allowNegatives); //returns true
}
}
Before you can assign a value to an instance property, the class should be instantiated, so first constructor will run and then you can assign property values.
That said, to have a better understanding of what is happening here, when you drop an instance of a control on your form at design time and set some of its properties, designer will generate a code like this:
private void InitializeComponent()
{
...
this.myControl1 = new MyControl();
...
//
// myControl1
//
this.myControl1.Location = new System.Drawing.Point(0, 0);
this.myControl1.Name = "myControl1";
this.myControl1.Size = new System.Drawing.Size(100, 22);
this.myControl1.MyProperty = true;
...
}
I believe it's now clear that what is happening here. You see first the constructor of your control will run, then later property values will be set.
To use property values to configure your object can put the logic inside the setter of the property:
private bool myProperty = false;
public bool MyProperty
{
get { return myProperty;}
set
{
myProperty = value;
// some logic here.
}
}
It's the most common scenario.
Another option is delaying the initializations to some time later, for example when the control handle is created by overriding OnHandleCreated or another suitable time.
// This is just an example, the event may not be a good one for your requirement
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
// some logic here
}
Another option for complex initialization scenarios which may involve multiple properties, you can implement ISupportInitialize and put the logic inside EndInit:
public class MyControl : TextBox, ISupportInitialize
{
public void BeginInit()
{
}
public void EndInit()
{
// some logic here
}
}
Then when you drop an instance of the control on the form, this code will be generated in addition to the common code that I showed at beginning of this answer:
...
((System.ComponentModel.ISupportInitialize)(this.myControl1)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
...
(I expect it's obvious now, that) All above options will run after running the constructor.
Putting that code in the setter worked
public partial class ControlIntEntry : TextBox
{
private bool allowNegatives = false;
[Description("Allow negative values"), Category("Behavior")]
public bool AllowNegatives
{
get { return allowNegatives; }
set
{
allowNegatives = value;
if (allowNegatives)
this.KeyPress += KeyPress_AllowNegatives;
else
this.KeyPress += KeyPress_PositiveOnly;
}
}
public ControlIntEntry()
{
InitializeComponent();
}
private void KeyPress_PositiveOnly(object sender, KeyPressEventArgs e)
{
Char newChar = e.KeyChar;
if (!Char.IsDigit(newChar) && newChar != 8)
{
e.Handled = true;
}
}
private void KeyPress_AllowNegatives(object sender, KeyPressEventArgs e)
{
Char newChar = e.KeyChar;
int cursorIndex = this.SelectionStart;
if (cursorIndex == 0)
{
if (!Char.IsDigit(newChar) && newChar != 8 && newChar != 45)
{
e.Handled = true;
}
}
else
{
if (!Char.IsDigit(newChar) && newChar != 8)
{
e.Handled = true;
}
}
}
}
I am building an application to teach myself MVVM and with some Googling (and some trial an error) I have managed to get to the point where I can open a second window from the ViewModel but not to pass a variable from one page to the other. This is my ViewModel.
public VendorSelectViewModel()
{
Ping ping = new Ping();
PingReply pingresult = ping.Send("192.168.1.10");
if (pingresult.Status.ToString() == "Success")
{
LoadVendorsAsync();
}
else
{
LoadVendors();
}
NextCommand = new RelayCommand(NextWindow);
}
public ICommand NextCommand { get; private set; }
void NextWindow()
{
Console.WriteLine(selectedVendor.VendorName);
Messenger.Default.Send(new NotificationMessage("NextWindow"));
}
In my view I have this
public VendorSelectWindow()
{
InitializeComponent();
_vm = new Biz.Invoicer.VendorSelectViewModel();
DataContext = _vm;
Messenger.Default.Register<NotificationMessage>(this, NotificationMessageReceived);
}
private void NotificationMessageReceived(NotificationMessage msg)
{
if (msg.Notification == "NextWindow")
{
var invoicerWindow = new InvoicerWindow();
invoicerWindow.Show();
}
}
So I know (or I think I know) this may not be a "Best Practice" but I will come back to this and refactor as I get to know the MVVM patern and MVVM Light better. Currently I am trying to pass a variable from the ViewModel of the first page (VendorSelectViewModel) to the Second page (InvoicerWindow) but I haven't managed to the syntax correct.
What do I need to do to pass a variable from one page to the next?
First of all you can pass an arbitrary object as the parameter of the IMessenger.Send<TMessage> method - the TMessage type parameter is not restricted. E.g.:
//ViewModel:
void NextWindow()
{
//...
int someValue = 10;
Messenger.Default.Send(someValue);
}
//View:
public VendorSelectWindow()
{
//...
Messenger.Default.Register<int>(this, MessageReceived);
}
private void MessageReceived(int value)
{
//...
}
If however you find the NotificationMessage class particularly useful in your case you could make use of the generic NotificationMessage<T> version, which exposes additional property Content of arbitrary type T:
//ViewModel:
void NextWindow()
{
//...
int someValue = 10;
Messenger.Default.Send(new NotificationMessage<int>(someValue, "Notification text"));
}
//View:
public VendorSelectWindow()
{
//...
Messenger.Default.Register<NotificationMessage<int>>(this, MessageReceived);
}
private void MessageReceived(NotificationMessage<int> message)
{
var someValue = message.Content;
//...
}
Or, if that does not suit you, you could create your own class deriving from NotificationMessage and exposing additional members and use that as the message object.
Instead of passing a NotificationMessage to the messenger, you could pass an instance of your own custom type which may carry as many values you want:
void NextWindow()
{
Console.WriteLine(selectedVendor.VendorName);
Messenger.Default.Send(new YourPayload() {WindowName = "NextWindow", Parameter = "some value..:");
}
...
public VendorSelectWindow()
{
InitializeComponent();
_vm = new Biz.Invoicer.VendorSelectViewModel();
DataContext = _vm;
Messenger.Default.Register<YourPayload>(this, NotificationMessageReceived);
}
private void NotificationMessageReceived(YourPayload msg)
{
if (msg.WindowName == "NextWindow")
{
string param = msg.Parameter;
var invoicerWindow = new InvoicerWindow();
invoicerWindow.Show();
}
}
YourPayload is a custom class with two properties, WindowName and Parameter.
I am trying to call the ZoomBy() method of SciChart control from ViewModel. The ZoomBy() is easily available in the xaml.cs file like below:
// TODO: Need to implement zoom using MVVM
private void BtnZoomIn_Click(object sender, RoutedEventArgs e)
{
TemperatureGraph.ChartModifier.XAxis.ZoomBy(-0.1, -0.1);
}
The same functionality I need to implement using the ViewModel pattern.
However the ZoomExtents method is easily being called using ViewportManager of SciChart control. E.g. below: XAML file
<RocheButton Name="BtnZoomOut" DockPanel.Dock="Top" Icon="{IconResource Icon=ZoomOut}" HorizontalAlignment="Right" Command="{Binding ZoomOutCommand}" />
<s:SciChartSurface x:Name="TemperatureGraph" Grid.Column="0" s:ThemeManager.Theme="BrightSpark"
RenderableSeries="{s:SeriesBinding TemperatureGraphViewModel}" DockPanel.Dock="Bottom"
ViewportManager="{Binding ViewportManager}">
And the ViewModel Code:
public class TemperatureSummaryGraphViewModel : ViewModelBase
{
#region Private Members
private IXyDataSeries<TimeSpan, double> TemperatureDataSeries = new XyDataSeries<TimeSpan, double>();
private IXyDataSeries<TimeSpan, double> AcquisitionPointDataSeries = new XyDataSeries<TimeSpan, double>();
private DefaultViewportManager _viewportManager = new DefaultViewportManager();
private ICommand _zoomOutCommand;
#endregion
#region Constructor
public TemperatureSummaryGraphViewModel()
{
ZoomOutCommand = new DelegateCommand(() => ZoomOutTemperatureGrpah());
GenerateDummySeries();
TemperatureGraphViewModel.Add(new LineRenderableSeriesViewModel()
{
DataSeries = TemperatureDataSeries,
StyleKey = "LineSeriesStyle0"
});
TemperatureGraphViewModel.Add(new XyScatterRenderableSeriesViewModel()
{
DataSeries = AcquisitionPointDataSeries,
StyleKey = "ScatterSeriesStyle0"
});
}
#endregion
#region Public Properties
public ObservableCollection<IRenderableSeriesViewModel> TemperatureGraphViewModel { get; } = new ObservableCollection<IRenderableSeriesViewModel>();
public IViewportManager ViewportManager
{
get
{
return _viewportManager;
}
set
{
if (ReferenceEquals(value, _viewportManager))
{
return;
}
_viewportManager = (DefaultViewportManager)value;
OnPropertyChanged("ViewportManager");
}
}
public ICommand ZoomOutCommand
{
get
{
return _zoomOutCommand;
}
set
{
if (ReferenceEquals(value, _zoomOutCommand))
{
return;
}
_zoomOutCommand = value;
OnPropertyChanged(nameof(ZoomOutCommand));
}
}
#endregion
#region Public Methods
/// <summary>
/// To generate dummy data
/// // TODO: Need to integrate it with RunEditor with the actual data
/// </summary>
public void GenerateDummySeries()
{
double y = 80.5, yVar = 30.0;
TemperatureDataSeries.Append(TimeSpan.FromMinutes(1), 40.0);
TemperatureDataSeries.Append(TimeSpan.FromMinutes(2), 80.5);
for (int x = 2; x < 50; x++)
{
TemperatureDataSeries.Append(TimeSpan.FromMinutes(x), y);
yVar *= -1;
y += yVar;
}
for (var i = 5.4; i < 50; i += 2)
{
AcquisitionPointDataSeries.Append(TimeSpan.FromMinutes(i), 60.0);
}
}
public void ZoomOutTemperatureGrpah()
{
_viewportManager.ZoomExtents();
}
#endregion
}
}
This code is working fine and zooming out the scichart control to 100%.
I want to implement the same using the ZoomBy().
Since the methods Zoom, ZoomBy, Scroll and ScrollTo only exist on IAxis and AxisBase derived classes, the only way to trigger these from a ViewModel is to actually pass the Axis instance into your ViewModel.
This is not true MVVM but it could be considered a workaround: by passing your Axis as IAxis to the ViewModel and controlling it directly.
The only other way is to write a behavior, or SciChart's favourite: a ChartModifier, which listens to events from your ViewModel and controls the axis directly.
This is the preferred approach. If you search for 'Call method on View from ViewModel' you will see the approach of raising events on ViewModel and handling in the View.
Currently I am using a class as follows to check if yhe TextBoxes on the form that I register to it, all have a non-blank text or not and it work fine, But now I want to also add a ComboBox to this validation so that validation should be done when none of the registered textboxes AND Combobxes on the form are blank.
So if I want to add a Combobx to this class, how should it look like? what is the best pracitce to do it?
public class InputValidator
{
public delegate void ValidationDoneDelegate(bool enable);
public event ValidationDoneDelegate ValidationDone;
public void RegisterTextBox(TextBox tb)
{
tb.TextChanged += (s, e) => this.Validate(s);
}
private void Validate(object sender)
{
var t = sender as TextBox;
if (t == null)
{
return;
}
var validationDone = ValidationDone;
if (validationDone != null)
{
validationDone(!string.IsNullOrEmpty(t.Text));
}
}
}
I have two lists setup which will hold all the TextBox and ComboBox references. When it is time to validate, we will check all of the registered controls and if ANY of them are empty, we will be invalid. I think you will also be able to see how this can easily be extended to support additional control types.
public class InputValidator
{
public delegate void ValidationDoneDelegate(bool enable);
public event ValidationDoneDelegate ValidationDone;
private List<TextBox> textBoxes = new List<TextBox>();
private List<ComboBox> comboBoxes = new List<ComboBox>();
public void RegisterTextBox(TextBox tb)
{
tb.TextChanged += (s, e) => this.Validate();
textBoxes.Add(tb);
}
public void RegisterComboBox(ComboBox cb)
{
cb.SelectedValueChanged += (s, e) => this.Validate();
comboBoxes.Add(cb);
}
private void Validate()
{
bool isValid = true;
foreach (var tb in textBoxes)
{
if (string.IsNullOrEmpty(tb.Text))
isValid = false;
}
if (isValid)
{
foreach (var cb in comboBoxes)
{
if (cb.SelectedItem == null)
isValid = false;
}
}
var validationDone = ValidationDone;
if (validationDone != null)
{
validationDone(isValid);
}
}
}
Now I'm not sure exactly what you consider to be invalid input for the ComboBox. So you may need to tweak this line to meet your needs: isValid = cb.SelectedItem != null;. I have assemed that as long as something is selected that the selection is valid.
EDIT: I had forgotten to switch the last line to validationDone(isValid);
I am using ListViewCollection class with my dataGrid. The underlying collection is an observable collection.
Whenever i call Move methods in the collection ( which is in ViewModel), the CurrentChanged Event doesnt fire.
However when UI calls the same method on it ( i can see it in the call stack), the event does fire.
this.EmailTemplates = new ListCollectionView(templateVmList);
this.EmailTemplates.CurrentChanging += (o, e) => EmailTemplates_CurrentChanging(o, e);
this.EmailTemplates.CurrentChanged += (o, e) => { this.SelectedEmailTemplate = (EmailTemplateViewModel)this.EmailTemplates.CurrentItem; };
if (this.EmailTemplates.Count > 0)
{
if (!this.EmailTemplates.MoveCurrentToFirst())
throw new ArgumentException("Element not found in collection");
}
What should i do in code to make sure the events fire no matter who is changing the collection.
Try using CollectionViewSource.GetDefaultView instead of creating a new ListCollectionView.
This test code worked fine for me
public class LcViewModel : BaseItemsViewModel
{
public LcViewModel()
{
MoveCommand = new RelayCommand(Move);
var view = CollectionViewSource.GetDefaultView(Items);
view.CurrentChanged += (sender, args) => Debug.WriteLine("CurrentChanged");
view.CurrentChanging += (sender, args) => Debug.WriteLine("CurrentChanging");
}
public ICommand MoveCommand { get; private set; }
private void Move()
{
var view = CollectionViewSource.GetDefaultView(Items);
view.MoveCurrentToFirst();
}
}