I am trying to create an interface where rows in a table can be viewed in a manner similar to this (from Power Apps). Is anybody aware of a way this can be achieved in WinForms? Maybe there are some custom controls I can download and add to the project?
I could in theory instantiate controls one by one. But that would take a while/it would be difficult to make that scrollable (or at least I don't know how). Thank you for any ideas.
For something light-duty, it's pretty easy to use a flow layout panel to show a bound table in a scrollable list. A quick proof-of-concept is shown below [clone].
For production use or with huge tables there are at least three major companies producing sophisticated custom WinForms components of this nature that have free trial licenses. IMO one "might" want to weight the benefits vs the cost.
class CustomFlowLayoutTable : FlowLayoutPanel
{
public CustomFlowLayoutTable()
{
AutoScroll = true;
Products.ListChanged += (sender, e) =>
{
switch (e.ListChangedType)
{
case ListChangedType.ItemAdded:
Controls.Add(Products[e.NewIndex]);
break;
default:
break;
}
};
}
public BindingList<ProductCard> Products = new BindingList<ProductCard>();
}
ProductCard
public partial class ProductCard : UserControl
{
int _id = 0;
public ProductCard()
{
InitializeComponent();
Name = $"userControl{_id++}"; // No space, start with lowercase
Padding = new Padding(0);
Margin = new Padding(2);
pictureBox.Padding = new Padding(8);
labelExp.Click += (sender, e) => MessageBox.Show($"{this}");
}
public string Description
{
get => labelDescription.Text;
set
{
if (!Equals(labelDescription.Text, value))
{
labelDescription.Text = value;
var imagePath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"Images",
$"{Description}.png"
);
// Set the "Copy to Output Directory" property of all image files.
if (File.Exists(imagePath))
{
pictureBox.Image = Image.FromFile(imagePath);
}
}
}
}
public string Category
{
get => labelCategory.Text;
set => labelCategory.Text = value;
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
Width = getWidth();
// Respond to width changes of owner container.
Parent.SizeChanged += (sender, e) => Width = getWidth();
}
int VSBW { get; } = SystemInformation.VerticalScrollBarWidth;
private int getWidth() =>
Parent.Width -
Parent.Padding.Left -
Parent.Padding.Right -
Margin.Left -
Margin.Right -
VSBW;
public override string ToString() =>
$"{Category}{Environment.NewLine}{Description}";
}
Main Form
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
flowLayoutPanel.Products.Add(new ProductCard
{
Category = "Carpet",
Description = "Caserta Stone Beige",
});
flowLayoutPanel.Products.Add(new ProductCard
{
Category = "Carpet",
Description = "Caserta Sky Grey",
});
flowLayoutPanel.Products.Add(new ProductCard
{
Category = "Carpet",
Description = "Ageless Beauty Clay",
});
flowLayoutPanel.Products.Add(new ProductCard
{
Category = "Carpet",
Description = "Lush II Tundra",
});
flowLayoutPanel.Products.Add(new ProductCard
{
Category = "Carpet",
Description = "Lush II Frosty Glade",
});
flowLayoutPanel.Products.Add(new ProductCard
{
Category = "Hardwood",
Description = "Bolivian Rosewood",
});
}
}
Related
I'm having issues updating the UI threads. Application is running 1 UI thread for each form, meaning just using SyncronizationContext with the UI thread doesn't work. I'm doing this for looping update performance as well as modal popup possibilities like select value before you can use the form.
How I'm creating it in ApplicationContext:
public AppContext()
{
foreach(var form in activeForms)
{
form.Load += Form_Load;
form.FormClosed += Form_FormClosed;
StartFormInSeparateThread(form);
//form.Show();
}
}
private void StartFormInSeparateThread(Form form)
{
Thread thread = new Thread(() =>
{
Application.Run(form);
});
thread.ApartmentState = ApartmentState.STA;
thread.Start();
}
There are controls on each for that are databound and updating with values from the same databound object. Controls being Labels and DataGridview (bound to a bindinglist).
What would be ideal is having the Bindinglist threadsafe and execute on these multiple UI threads. Found some examples that I attempted like this:
List<SynchronizationContext> listctx = new();
public ThreadSafeBindingList2()
{
//SynchronizationContext ctx = SynchronizationContext.Current;
//listctx.Add(ctx);
}
public void SyncContxt()
{
SynchronizationContext ctx = SynchronizationContext.Current;
listctx.Add(ctx);
}
protected override void OnAddingNew(AddingNewEventArgs e)
{
for (int i = 0; i < listctx.Count; i++)
{
if (listctx[i] == null)
{
BaseAddingNew(e);
}
else
{
listctx[i].Send(delegate
{
BaseAddingNew(e);
}, null);
}
}
}
void BaseAddingNew(AddingNewEventArgs e)
{
base.OnAddingNew(e);
}
protected override void OnListChanged(ListChangedEventArgs e)
{
for (int i = 0; i < listctx.Count; i++)
{
if (listctx[i] == null)
{
BaseListChanged(e);
}
else
{
listctx[i].Send(delegate
{
BaseListChanged(e);
}, null);
}
}
}
void BaseListChanged(ListChangedEventArgs e)
{
base.OnListChanged(e);
}
I'm also using a static class as a data property change hub for all controls so I don't change the databinding source more than once (again due to performance), where I have a background worker "ticking" every 1-3 seconds depending on system load:
private static void BackgroundWorker_DoWork(object? sender, DoWorkEventArgs e)
{
if (timerStart is false)
{
Thread.Sleep(6000);
timerStart = true;
}
while (DisplayTimerUpdateBGW.CancellationPending is false)
{
//UIThread.Post((object stat) => //Send
//{
threadSleepTimer = OrderList.Where(x => x.Status != OrderOrderlineStatus.Claimed).ToList().Count > 20 ? 2000 : 1000;
if (OrderList.Count > 40)
threadSleepTimer = 3000;
UpdateDisplayTimer();
//}, null);
Thread.Sleep(threadSleepTimer);
}
}
private static void UpdateDisplayTimer()
{
var displayLoopStartTimer = DateTime.Now;
TimeSpan displayLoopEndTimer = new();
Span<int> orderID = CollectionsMarshal.AsSpan(OrderList.Select(x => x.ID).ToList());
for (int i = 0; i < orderID.Length; i++)
{
OrderModel order = OrderList[i];
order.OrderInfo = "Ble";
Span<int> OrderLineID = CollectionsMarshal.AsSpan(order.Orderlines.Select(x => x.Id).ToList());
for (int j = 0; j < OrderLineID.Length; j++)
{
OrderlineModel ol = order.Orderlines[j];
TimeSpan TotalElapsedTime = ol.OrderlineCompletedTimeStamp != null ? (TimeSpan)(ol.OrderlineCompletedTimeStamp - ol.OrderlineReceivedTimeStamp) : DateTime.Now - ol.OrderlineReceivedTimeStamp;
string displaytimerValue = "";
if (ol.OrderlineCompletedTimeStamp == null)
displaytimerValue = TotalElapsedTime.ToString(#"mm\:ss");
else
displaytimerValue = $" {(DateTime.Now - ol.OrderlineCompletedTimeStamp)?.ToString(#"mm\:ss")} \n({TotalElapsedTime.ToString(#"mm\:ss")})";
ol.DisplayTimer = displaytimerValue;
}
}
}
Ideally I want to have the labels and datagridview properties databindings so that I can have INotifyPropertyChanged just updating these relevant properties in all UI threads.
Any help would be appreciated!
One of many ways to look at this is that there's only one display area (albeit which might consist of many screens) and only one element of it can change at any given moment. To my way of thinking, this means that having more than one UI thread can often be self defeating (unless your UI is testing another UI). And since the machine has some finite number of cores, having a very large number of threads (whether of the UI or worker variety) means you can start to have a lot of overhead marshalling the context as threads switch off.
If we wanted to make a Minimal Reproducible Example that has 10 Form objects executing continuous "mock update" tasks in parallel, what we could do instead of the "data property change hub" you mentioned is to implement INotifyPropertyChanged in those form classes with static PropertyChanged event that gets fired when the update occurs. To mock data binding where FormWithLongRunningTask is the binding source, the main form subscribes to the PropertyChanged event and adds a new Record to the BindingList<Record> by identifying the sender and inspecting e to determine which property has changed. In this case, if the property is TimeStamp, the received data is marshalled onto the one-and-only UI thread to display the result in the DataGridView.
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// Subscribe to the static event here.
FormWithLongRunningTask.PropertyChanged += onAnyFWLRTPropertyChanged;
// Start up the 10 forms which will do "popcorn" updates.
for (int i = 0; i < 10; i++)
{
new FormWithLongRunningTask { Name = $"Form{i}" }.Show(this);
}
}
private void onAnyFWLRTPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (sender is FormWithLongRunningTask form)
{
BeginInvoke(() =>
{
switch (e.PropertyName)
{
case nameof(FormWithLongRunningTask.TimeStamp):
dataGridViewEx.DataSource.Add(new Record
{
Sender = form.Name,
TimeStamp = form.TimeStamp,
});
break;
default:
break;
}
});
}
}
}
The DataGridView on the main form uses this custom class:
class DataGridViewEx : DataGridView
{
public new BindingList<Record> DataSource { get; } = new BindingList<Record>();
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if (!DesignMode)
{
base.DataSource = this.DataSource;
AllowUserToAddRows = false;
#region F O R M A T C O L U M N S
DataSource.Add(new Record());
Columns[nameof(Record.Sender)].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
var col = Columns[nameof(Record.TimeStamp)];
col.AutoSizeMode = DataGridViewAutoSizeColumnMode.AllCells;
col.DefaultCellStyle.Format = "hh:mm:ss tt";
DataSource.Clear();
#endregion F O R M A T C O L U M N S
}
}
protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e)
{
base.OnCellPainting(e);
if ((e.RowIndex > -1) && (e.RowIndex < DataSource.Count))
{
var record = DataSource[e.RowIndex];
var color = _colors[int.Parse(record.Sender.Replace("Form", string.Empty))];
e.CellStyle.ForeColor = color;
if (e.ColumnIndex > 0)
{
CurrentCell = this[0, e.RowIndex];
}
}
}
Color[] _colors = new Color[]
{
Color.Black, Color.Blue, Color.Green, Color.LightSalmon, Color.SeaGreen,
Color.BlueViolet, Color.DarkCyan, Color.Maroon, Color.Chocolate, Color.DarkKhaki
};
}
class Record
{
public string Sender { get; set; } = string.Empty;
public DateTime TimeStamp { get; set; }
}
The 'other' 10 forms use this class which mocks a binding source like this:
public partial class FormWithLongRunningTask : Form, INotifyPropertyChanged
{
static Random _rando = new Random(8);
public FormWithLongRunningTask() => InitializeComponent();
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
_ = runRandomDelayLoop();
}
private async Task runRandomDelayLoop()
{
while(true)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(_rando.NextDouble() * 10));
TimeStamp = DateTime.Now;
Text = $"# {TimeStamp.ToLongTimeString()}";
BringToFront();
}
catch (ObjectDisposedException)
{
}
}
}
DateTime _timeStamp = DateTime.Now;
public DateTime TimeStamp
{
get => _timeStamp;
set
{
if (!Equals(_timeStamp, value))
{
_timeStamp = value;
OnPropertyChanged();
}
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged
{
add => PropertyChanged += value;
remove => PropertyChanged -= value;
}
public static event PropertyChangedEventHandler? PropertyChanged;
}
I believe that there's no 'right' answer to your question but I hope there's something here that might move things forward for you.
When we enter value in row 1 the value entered in row 1 is appearing back in row 6 when we scroll to the row 6. Please see the below code and advice.
namespace Kites
{
public class Marks
{
// add any if you need more
public string StudentName { get; set; }
public string MarksScored { get; set; }
}
public class TEXTCHECK
{
public int POS { get; set; }
public string Value { get; set; }
}
public class MarksListViewAdapter : BaseAdapter<Marks>
{
private List<Marks> mstuduentmarks;
private List<TEXTCHECK> abc = new List<TEXTCHECK>();
private Context mcontext;
public MarksListViewAdapter (Context context, List<Marks> stud)
{
mstuduentmarks = stud;
mcontext = context;
}
public override int Count
{
get
{
return mstuduentmarks.Count;
// return mattendence.Count;
}
}
public override long GetItemId (int position)
{
return position;
}
public override Marks this[int position]
{
get
{
return mstuduentmarks [position];
// return mattendence [position];
}
}
class ViewHolder : Java.Lang.Object
{
public EditText comsevin;
public TextView namenmn;
}
public override View GetView (int position, View convertView, ViewGroup parent)
{
ViewHolder holder;
View view = convertView;
if (view == null) // otherwise create a new one
{
view = LayoutInflater.From(mcontext).Inflate(Resource.Layout.listview_Marks, null, false);
holder = new ViewHolder();
holder.comsevin = view.FindViewById<EditText>(Resource.Id.editTextTeacherMarks);
holder.namenmn = view.FindViewById<TextView>(Resource.Id.textStudentNameTeacherMarks);
holder.namenmn.Tag = position;
view.Tag = holder;
}
else
{
holder = (ViewHolder)view.Tag;
}
holder.namenmn.Text = mstuduentmarks[position].StudentName;
int pos = (int)holder.namenmn.Tag;
holder.comsevin.TextChanged += (sender, e) =>
{
abc[pos].Value = holder.comsevin.Text;
};
//TextView txtStudent =
//txtStudent.Text = mstuduentmarks[position].StudentName;
//txtMarks.FocusChange += (object sender, View.FocusChangeEventArgs e) =>
//{
// //txtMarks.RequestFocusFromTouch ();
// mstuduentmarks[position].MarksScored = txtMarks.Text;
//};
holder.comsevin.BeforeTextChanged += (sender, e) =>
{
abc.Add(new TEXTCHECK { POS = position, Value = mstuduentmarks[position].MarksScored });
};
holder.comsevin.AfterTextChanged += (sender, e) =>
{
int a = abc[pos].POS;
mstuduentmarks[pos].MarksScored = abc[pos].Value;
};
//txtMarks.Tag = position;
//txtMarks.TextChanged += TxtMarks_TextChanged;
return view;
}
//void TxtMarks_TextChanged (object sender, Android.Text.TextChangedEventArgs e)
//{
// EditText txtMarks = (EditText)sender;
// //var position = (int)txtMarks.Tag;
//}
}
}
When we enter value in row 1 the value entered in row 1 is appearing back in row 6 when we scroll to the row 6. Please see the below code and advice.
As a rule of thumb, when experiencing lists that don't reflect the dataset (experiencing item repetition for example) in listview / recyclerview it means that you're either using dirty views which were previously used and then uncorrectly Re-Bound, or simply using wrong positions during bind
I see where you are getting it wrong:
if (view == null) // otherwise create a new one
{
view = LayoutInflater.From(mcontext).Inflate(Resource.Layout.listview_Marks, null, false);
holder = new ViewHolder();
holder.comsevin = view.FindViewById<EditText>(Resource.Id.editTextTeacherMarks);
holder.namenmn = view.FindViewById<TextView>(Resource.Id.textStudentNameTeacherMarks);
holder.namenmn.Tag = position;//<------------here!!!
view.Tag = holder;
}
TLDR Don't save positions this way.
Whats happening: this instance of your view is being reused by listView, meaning that sometimes (many times) if (view == null) will be false and this means Tag property will not be updated for row 6 (or any other calls that will use recycled Views) and you are in fact using a dirty value.
You are then trying to use the Tag property as position, but forgetting this tag is already dirty if the view was recycled
int pos = (int)holder.namenmn.Tag;
holder.comsevin.TextChanged += (sender, e) =>
{
abc[pos].Value = holder.comsevin.Text;
};
Since you have access to the position in this method call you should use it directly
take a look at this guide from Java Code geeks even though it's in Java you will be able to see a good implementation of the old ViewHolder/ListView pattern.
Hope this helps
I have trying to build a simulator of Ingenico POS terminal (iWL220).
The main screen I have a combo-box. Once user enter id and password the combo-box load 6 menus. If User click btn1 then combo-box clear the menu and add another set of menu. If user click btn1 for new loaded menu then again combo-box cleared and load another set of menu so on.
My problem is for each button click (btn1, btn2, btn3, btn4, btn5) I have to code lot of if else statement. Example;
First menu (on combo-box) has 6 sector.
1.SectorA
2.SectorB
3.SectorC
4.SectorD
5.SectorE
6.SectorF
If user choose 1.SectorA then user click btn1. Then btn1 clear the combo-box and loads another set of menu. This time menu (on combo-box) has 3 companies.
1.CompanyA
2.CompanyB
3.CompanyC
This time user choose 1.CompanyA then user click again btn1. Then btn1 clear the combo-box and loads another set of menu. This time menu (on combo-box) has 2 payment option.
1.FullPayment
2.ParitalPayment
Now this time if user click btn1 or btn2 the combo-box visible become false and in main screen there is a label and text box. The text box allows user to enter the subscriber number and press enter (green button).
I already load Ingenico terminal picture as jpeg and top of it I set my buttons.
I gave only small version of my simulation. In my app there are 114 probability that user can choose.
In my app btn1 has 92 probability to be clicked, btn2 has 53 probability to be clicked and so on. After user enters the subscriber number and click green button the my app use wfc services to format the data and send to sql server.
But before user click each combination of button some where in my app I store the btn number as 422. This 422 means, user chose SectorD + CompanyB + ParitalPayment option. So my wfc will know what it is mean 422.
My question is what is the shortest way to construct my button events for this 114 probability case?
I have 4 buttons. Btn1, Btn2, Btn3 and Btn4. Also I have some arrays as it shown below and 1 combo-box.
1.ArrayMain() = {“1.Water”,”2.Air”,”3.Soil”,”4.Fire”}
1.1. ArrayWater() = {“1.Salty”,”2.Fresh”, “3.Contaminated”}
1.1.1.ArraySalty() = {1.”AA”, 2.”BB”, 3.”CC”}
1.1.2.ArrayFresh() = {1.”DD”, 2.”EE”, 3.”FF”}
1.1.3.ArrayContaminated() = {1.”XX”, 2.”YY”, 3.”ZZ”}
1.2 ArrayAir() = {“1.Fresh”, “2.Contaminated”}
1.3 ArraySoil() = {“1.Normal”, “2.Contaminated”}
1.4 ArrayFire() = {“1.Low”,”2.Mid”,”3.High”}
When my app starts, first array values 1.(ArrayMain) fills the comboBox. This comboBox will have 4 values as, “1.Water”, ”2.Air”, ”3.Soil”, ”4.Fire” in it. If user choose “1.Water” than user clicks Btn1. Than btn1 events clears the comboBox and loads 1.1ArrayWater() values into comboBox.
Second time if user chooses “1.Salty” than user clicks again btn1 and this time btn1 events clears the comboBox and loads 1.1.1ArraySalty() values into comboBox.
Third time if user chooses “2.BB” than user clicks Btn2 and sends the information “BB” for calculation.
First you have 5 (more or less) menu item and each time you press any (number) buttons (1 to 9 lilke in pos terminal) pressed than new menu appears on the screen.
Each button at any specific time shall execute some specific action depending on the state of the system. Obviously, if you try to decide the specific action depending on the multitude of different variables, you will create a lot of branching code. Such code is very difficult to write correctly and even more difficult to debug and maintain.
So, what if we encapsulate current action for each possible state(sequence of state) in some specific class (interface):
/// <summary>
/// Represents internal terminal presenter that is used inside IGlobalTerminalPresenter.
/// </summary>
public interface ITerminalPresenter
{
void UpdateUI();
ITerminalPresenter this[Int32 index]
{
get;
}
ITerminalPresenter Do1();
ITerminalPresenter Do2();
ITerminalPresenter Parent
{
get;
set;
}
void Reset();
}
Inside the form we will use field of a similar interface that will encapsulate all changes of the presenter.
/// <summary>
/// Represents terminal presenter that UI can operate upon.
/// </summary>
public interface IGlobalTerminalPresenter
{
void UpdateUI();
void Do1();
void Do2();
Int32 SelectedIndex
{
get;
set;
}
void Reset();
}
Our event handlers will become:
private void comboBox_SelectedIndexChanged(object sender, EventArgs e)
{
var senderComboBox = (ComboBox)sender;
this.globalTerminalPresenter.SelectedIndex = senderComboBox.SelectedIndex;
}
private void button1_Click(object sender, EventArgs e)
{
this.globalTerminalPresenter.Do1();
}
private void button2_Click(object sender, EventArgs e)
{
this.globalTerminalPresenter.Do2();
}
To allow our concrete TerminalPresenters to interoperate with form we will force our form to implement the following interface:
/// <summary>
/// This represents your UI in technology-independent manner
/// </summary>
public interface ITerminalView
{
String Title { get; set; }
String Input { get; set; }
String Output { get; set; }
String Button1_Text { get; set; }
String Button2_Text { get; set; }
IEnumerable<String> SelectionItems { get; set; }
void Clear();
}
public partial class MainForm : Form,
ITerminalView
{
...
#region ITerminalView implementation
public string Title
{
get { return this.Text; }
set { this.Text = value; }
}
public String Button1_Text
{
get { return this.button1.Text; }
set { this.button1.Text = value; }
}
public String Button2_Text
{
get { return this.button2.Text; }
set { this.button2.Text = value; }
}
public string Input
{
get { return this.textBox_Input.Text; }
set { this.textBox_Input.Text = value; }
}
public string Output
{
get { return this.textBox_Output.Text; }
set { this.textBox_Output.Text = value; }
}
public IEnumerable<string> SelectionItems
{
get { return this.comboBox.Items.Cast<String>(); }
set
{
this.comboBox.Items.Clear();
if (value == null)
return;
foreach (var item in value)
{
this.comboBox.Items.Add(item);
}
}
}
public void Clear()
{
this.comboBox.SelectedIndex = -1;
this.Title = String.Empty;
this.Input = String.Empty;
this.Output = String.Empty;
this.SelectionItems = null;
}
#endregion
For now we will create two TerminalPresenters - one to just allow selection of next option through combobox, one that calculates sum of two numbers. Both of them use the same base class.
/// <summary>
/// Base class for all presenters
/// </summary>
public abstract class TerminalPresenterBase : ITerminalPresenter
{
protected ITerminalView view;
public TerminalPresenterBase(ITerminalView view)
{
if (view == null)
throw new ArgumentNullException("view");
this.view = view;
this.Parent = this;
}
public abstract void UpdateUI();
public abstract ITerminalPresenter this[int index]
{
get;
}
public abstract ITerminalPresenter Do1();
public abstract ITerminalPresenter Do2();
public virtual ITerminalPresenter Parent
{
get;
set;
}
public virtual void Reset()
{
this.UpdateUI();
}
}
/// <summary>
/// Presenter whose sole goal is to allow user to select some other option and press next
/// </summary>
public class SelectOptionPresenter : TerminalPresenterBase
{
private IList<KeyValuePair<String, ITerminalPresenter>> options;
private ITerminalPresenter selected;
private String title;
public SelectOptionPresenter(ITerminalView view,
String title,
IList<KeyValuePair<String, ITerminalPresenter>> options)
: base(view)
{
if (options == null)
throw new ArgumentNullException("options");
this.title = title;
this.options = options;
foreach (var item in options)
{
item.Value.Parent = this;
}
}
public override void UpdateUI()
{
this.view.Clear();
this.view.Button1_Text = "Confirm selection";
this.view.Button2_Text = "Go back";
this.view.Title = title;
this.view.SelectionItems = options
.Select(opt => opt.Key);
}
public override ITerminalPresenter this[int index]
{
get
{
this.selected = this.options[index].Value;
return this;
}
}
public override ITerminalPresenter Do1()
{
return this.ConfirmSelection();
}
public override ITerminalPresenter Do2()
{
return this.GoBack();
}
public ITerminalPresenter ConfirmSelection()
{
this.selected.UpdateUI();
return this.selected;
}
public ITerminalPresenter GoBack()
{
this.Parent.UpdateUI();
return this.Parent;
}
}
public enum APlusBState
{
EnterA,
EnterB,
Result
}
public class StepActions
{
public Action UpdateUI { get; set; }
public Func<ITerminalPresenter> Do1 { get; set; }
public Func<ITerminalPresenter> Do2 { get; set; }
}
public class APlusBPresenter : TerminalPresenterBase
{
private Int32 a, b;
private APlusBState state;
private String error = null;
private Dictionary<APlusBState, StepActions> stateActions;
private void InitializeStateActions()
{
this.stateActions = new Dictionary<APlusBState, StepActions>();
this.stateActions.Add(APlusBState.EnterA,
new StepActions()
{
UpdateUI = () =>
{
this.view.Title = this.error ?? "Enter A";
this.view.Input = this.a.ToString();
this.view.Button1_Text = "Confirm A";
this.view.Button2_Text = "Exit";
},
Do1 = () => // Confirm A
{
if (!Int32.TryParse(this.view.Input, out this.a))
{
this.error = "A is in incorrect format. Enter A again";
return this;
}
this.error = null;
this.state = APlusBState.EnterB;
return this;
},
Do2 = () => // Exit
{
this.Reset();
return this.Parent;
}
});
this.stateActions.Add(APlusBState.EnterB,
new StepActions()
{
UpdateUI = () =>
{
this.view.Title = this.error ?? "Enter B";
this.view.Input = this.b.ToString();
this.view.Button1_Text = "Confirm B";
this.view.Button2_Text = "Back to A";
},
Do1 = () => // Confirm B
{
if (!Int32.TryParse(this.view.Input, out this.b))
{
this.error = "B is in incorrect format. Enter B again";
return this;
}
this.error = null;
this.state = APlusBState.Result;
return this;
},
Do2 = () => // Back to a
{
this.state = APlusBState.EnterA;
return this;
}
});
this.stateActions.Add(APlusBState.Result,
new StepActions()
{
UpdateUI = () =>
{
this.view.Title = String.Format("The result of {0} + {1}", this.a, this.b);
this.view.Output = (this.a + this.b).ToString();
this.view.Button1_Text = "Exit";
this.view.Button2_Text = "Back";
},
Do1 = () => // Exit
{
this.Reset();
return this.Parent;
},
Do2 = () => // Back to B
{
this.state = APlusBState.EnterB;
return this;
}
});
}
public APlusBPresenter(ITerminalView view) : base(view)
{
this.InitializeStateActions();
this.Reset();
}
public override void UpdateUI()
{
this.view.Clear();
this.stateActions[this.state].UpdateUI();
}
public override ITerminalPresenter this[int index]
{
get { throw new NotImplementedException(); }
}
public override ITerminalPresenter Do1()
{
var nextPresenter = this.stateActions[this.state].Do1();
nextPresenter.UpdateUI();
return nextPresenter;
}
public override ITerminalPresenter Do2()
{
var nextPresenter = this.stateActions[this.state].Do2();
nextPresenter.UpdateUI();
return nextPresenter;
}
public override void Reset()
{
this.state = APlusBState.EnterA;
this.a = 0;
this.b = 0;
this.error = null;
}
}
/// <summary>
/// Represents terminal presenter to use inside GUI. It handles current ISpecificTerminalPresenter inside itself.
/// </summary>
public class GlobalTerminalPresenter : IGlobalTerminalPresenter
{
#region Fields
private ITerminalPresenter current;
private Int32 selectedIndex;
#endregion
#region Constructors
public GlobalTerminalPresenter(ITerminalPresenter mainPresenter)
{
if (mainPresenter == null)
throw new ArgumentNullException("mainPresenter");
this.current = mainPresenter;
this.UpdateUI();
}
#endregion
public void UpdateUI()
{
this.current.UpdateUI();
}
public void Do1()
{
this.current = this.current.Do1();
}
public void Do2()
{
this.current = this.current.Do2();
}
public Int32 SelectedIndex
{
get
{
return this.selectedIndex;
}
set
{
this.selectedIndex = value;
if (value == -1)
return;
this.current = this.current[value];
}
}
public void Reset()
{
this.current.Reset();
}
}
Then we initialize them in the constructor of our form:
public partial class MainForm : Form,
ITerminalView
{
private IGlobalTerminalPresenter globalTerminalPresenter;
public MainForm()
{
InitializeComponent();
var nextLevelPresenters = new KeyValuePair<String, ITerminalPresenter>[]
{
new KeyValuePair<String, ITerminalPresenter>(
"A plus B",
new APlusBPresenter(this)),
new KeyValuePair<String, ITerminalPresenter>(
"Just empty selector",
new SelectOptionPresenter(this,
"Selector with no selection choices",
Enumerable
.Empty<KeyValuePair<String, ITerminalPresenter>>()
.ToArray()))
};
var topPresenter = new SelectOptionPresenter(this, "Select the option and press the confirm button", nextLevelPresenters);
this.globalTerminalPresenter = new GlobalTerminalPresenter(topPresenter);
}
P.S.1: These code snippets assume that you have form named MainForm that has two buttons - button1, button2, one combobox, two textBoxes - textBox_Input, textBox_Output.
P.S.2: The pattern used is close enough to Model-View-Presenter, just without DataBindings.
P.S.3 You can create more or less generic state machine Presenters if you modify APlusBPresenter code. Or try to shape up ChainXxxx... classes and interfaces.
P.S.4: And sorry for these walls of code. That's probably too much for [SO] format, so I've put ad-hoc proof of concept at GitHub - https://github.com/Podskal/StackOverflow_29870164.git. It is ugly in many aspects, but as it is, it can at least give few ideas about how to implement your own system.
P.S.5: There are a lot of problematic places in this code, so you should very carefully consider how you will build your own system from it.
The problem is as follows:
In my User Control i have Label List (List ) which fills adding Labels which UC through a "Smart Tag" (using a DesignerActionMethodItem).
The problem is that it works correctly when we are in design time, for example, I add 3 items at design time, but when I test the application these items disappear as if they had never added.
P.S.:
I have:
MyControl class,
[Designer(typeof(MenuItemPanelDesigner ))]
public partial class MenuItemPanel : UserControl
{
private List<Label> _listaItems;
public MenuItemPanel()
{
InitializeComponent();
}
public List<Label> ListaItems
{
get
{
if (this._listaItems == null)
{
this._listaItems = new List<Label>();
}
return this._listaItems;
}
}
public void AgregarItem()
{
Label nuevoItem = new Label();
nuevoItem.Text = "Item " + this._listaItems.Count;
nuevoItem.AutoSize = false;
nuevoItem.TextAlign = ContentAlignment.MiddleCenter;
nuevoItem.Cursor = Cursors.Hand;
this.ListaItems.Add(nuevoItem);
this.Controls.Add(nuevoItem);
nuevoItem.Dock = DockStyle.Top;
nuevoItem.Height = 50;
}
}
MyControlDesigner class
class MenuItemPanelDesigner : System.Windows.Forms.Design.ControlDesigner
{
private DesignerActionListCollection actionLists;
public override DesignerActionListCollection ActionLists
{
get
{
if (null == actionLists)
{
actionLists = new DesignerActionListCollection();
actionLists.Add(new MenuItemPanelDesignerActionList(this.Component));
}
return actionLists;
}
}
}
and MyControlDesignerActionList
class MenuItemPanelDesignerActionList : DesignerActionList
{
private MenuItemPanel colUserControl;
private DesignerActionUIService designerActionUISvc = null;
//The constructor associates the control with the smart tag list.
public MenuItemPanelDesignerActionList(IComponent component) : base(component)
{
this.colUserControl = (MenuItemPanel)component;
this.designerActionUISvc = (DesignerActionUIService)GetService(typeof(DesignerActionUIService));
}
// Implementation of this abstract method creates smart tag items,
// associates their targets, and collects into list.
public override DesignerActionItemCollection GetSortedActionItems()
{
DesignerActionItemCollection items = new DesignerActionItemCollection();
//Define static section header entries.
items.Add(new DesignerActionHeaderItem("Items"));
items.Add(new DesignerActionMethodItem(this,"AgregarItem","Agregar Item"));
return items;
}
// Metodos
public void AgregarItem()
{
this.colUserControl.AgregarItem();
}
}
Hi coders I have yet another question involving data binding in winforms. I set up a test applications where I have a bindinglist composed of structs called CustomerInfo. I have bound a listbox control to this list and spun a thread to add CustomerInfo items to the bindinglist.
namespace dataBindingSample {
public partial class Form1 : Form {
public BindingList<CustomerInfo> stringList = new BindingList<CustomerInfo>();
public Thread testThread;
public Form1() {
InitializeComponent();
stringList.AllowNew = true;
stringList.RaiseListChangedEvents = true;
listBox1.DataSource = stringList;
testThread = new Thread(new ThreadStart(hh_net_retask_request_func));
testThread.Priority = ThreadPriority.Normal;
}
private void hh_net_retask_request_func() {
int counter = 1;
while (true) {
CustomerInfo cust = new CustomerInfo();
cust.Name = "Customer "+ counter.ToString();
this.Invoke((MethodInvoker)delegate {
stringList.Add(cust);
});
counter++;
Thread.Sleep(1000);
}
}
private void Form1_Load(object sender, EventArgs e) {
testThread.Start();
}
}
public struct CustomerInfo {
public string Name {
set {
name = value;
}
get {
return name;
}
}
private string name;
}
}
What I see in the list box is the name of the struct dataBindingSample.CustomerInfo as opposed to the property of the struct. I was under the impression that non complex binding took the first available property.
Please educate me as to what I am doing wrong.
Thanks,
You'll need to either add an override of ToString() to your CustomerInfo class that returns what you'd like displyed in your list box, or set listBox1.DisplayMemer = "Name" before setting the DataSource.