Search code examples
c#winformsbindingdynamic-programming

How to bind dynamically created text box?


I'm trying to create pricelist for hotel. I'm having a list of dates, and list of room types in hotel. That lists can contain random number of elements. That is created dynamically, and here is the code:

private void CreateControls() { var colIndex = 0;        
var vrsteSoba = _Presenter.VrstaSobeDto.ToArray();

        foreach (var bindingItem in vrsteSoba)
        {

            var lbl = new Label()
            {
                Width = LABEL_WIDTH,
                Height = LABEL_HEIGHT - 5,
                Left = 10,
                Top = 30 + colIndex * (EDIT_BOX_HEIGHT + SPACE_BETWEEN_CONTROL),
                Text = bindingItem
            };
            _dataPanel.Controls.Add(lbl);
            colIndex++;
        }

        int a = 1;

        foreach (var date in _Presenter.CeneTarifa)
        {
            int y = 0;

            var panel = new Panel
            {
                Height = PANEL_HEIGHT * (vrsteSoba.Length-4),
                Width = EDIT_BOX_WIDTH,
                Left = a * (EDIT_BOX_WIDTH + SPACE_BETWEEN_CONTROL + 50),
                Top = 5
            };

            _dataPanel.Controls.Add(panel);

            var label = new Label
            {
                Height = EDIT_BOX_HEIGHT,
                Location = new Point(0, 10),                    
                Text = date.Datum,
                Margin = new Padding(0)                    
            };

            panel.Controls.Add(label);

            int index = 0;
            
            foreach (var item in date.VrstaSobeCena)
            {
                var box = new TextBox();
                panel.Controls.Add(box);
                box.Height = EDIT_BOX_HEIGHT;
                box.Width = EDIT_BOX_WIDTH;
                box.Location = new Point(0, 30 + y * (EDIT_BOX_HEIGHT + SPACE_BETWEEN_CONTROL));
                box.DataBindings.Add(new Binding(nameof(box.Text), date, date.Cena[index].Cena1)); 
                
                y++;
                index++;                    
            }
            ++a;
        }
        _dataPanel.AutoScroll = true;
    }`

Here is image of that representation. PriceList

Now I'm facing a problem of data binding. I need to bind price, two way, for each text box. And I'm stuck.

I have tried to bind it to property name, but then all boxes get same value. If I try to bind it to value via index, I'm getting error Cannot bind to the property or column 34 on the DataSource. Parameter name: dataMember

Code below is used to fill model that is used in presenter

` private void FillCenePoTarifi() { var sobeArr = VrstaSobeDto.ToArray();
 foreach (var datum in Datumi)
        {    
            var dictionary = new Dictionary<string, decimal>();
            var cene = new List<Cena>();
            foreach (var item in sobeArr)
            {
                var tarif = _Tarife.Where(x => x.SifTarife == item).FirstOrDefault();

                if (tarif != null)
                    _SastavTarife = HotelierServerLocal.Default.TarifaViewBlo.GetSastaveTarife(tarif.IdTarife);

                //proveriti ovu logiku
                var cena = _SastavTarife.Where(x => x.Cena1 != 0).Select(c => c.Cena1).FirstOrDefault();

                cene.Add(new Cena { Cena1 = cena.ToString()});
                dictionary.Add(item, cena);
            }

            var model = new CenePoTarifi
            {
                Datum = datum,
                VrstaSobeCena = dictionary,
                Cena = cene
            };

            CeneTarifa.Add(model);
        }
    }`

Finally here are classes that use as model.

` public class CenePoTarifi{
public Dictionary<string, decimal> VrstaSobeCena { get; set; } = new Dictionary<string, decimal>();
    public string Datum { get; set; }

    private List<Cena> _Cena;


    public List<Cena> Cena
    {
        get => _Cena;
        set
        {
            _Cena = value;
            NotifyPropertyChanged("Cena");
        }
    }

public class Cena : 
{
    private string _Cena1;
    public string Cena1
    {
        get => _Cena1;
        set 

         {
            _Cena = value;
            NotifyPropertyChanged("Cena1");
          }


    }
}`

Does anyone has any suggestions?


Solution

  • Your question is: How to bind dynamically created text box. Here is one tested way for accomplishing that specific task.

    First create some textboxes dynamically:

    public MainForm()
    {
        InitializeComponent();
        buttonRandom.Click += (sender, e) => generateRandomList();
    }
    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        List<TextBox> tmp = new List<TextBox>();
        for (int column = 1; column < tableLayoutPanel.ColumnCount; column++)
        {
            for (int row = 1; row < tableLayoutPanel.RowCount; row++)
            {
                TextBox textBox = new TextBox { Anchor = (AnchorStyles)0xF };
                tableLayoutPanel.Controls.Add(textBox, column, row);
                tmp.Add(textBox);
                textBox.KeyDown += onAnyTextBoxKeyDown;
            }
        }
        _textboxes = tmp.ToArray();
        // Generate first dataset
        generateRandomList();
    }
    TextBox[] _textboxes = null;
    

    table layout panel


    Then, whenever a new random list is generated, clear any old text and databindings from every TextBox before creating a new data binding for it.

    public static Random Rando { get; } = new Random(2);
    private void generateRandomList()
    {
        // Clear ALL the data + bindings for ALL the textboxes.
        foreach (var textbox in _textboxes)
        {
            textbox.Clear();
            textbox.DataBindings.Clear();
        }
        // Generate and create new bindings
        int count = Rando.Next(1, 79);
        for (int i = 0; i < count; i++)
        {
            var textbox = _textboxes[i];
            VrstaSobeCena vrstaSobeCena =
                new VrstaSobeCena{ Sobe = (Sobe)tableLayoutPanel.GetRow(textbox) };
            textbox.Tag = vrstaSobeCena;
            textbox.DataBindings.Add(
                new Binding(
                    nameof(TextBox.Text),
                    vrstaSobeCena,
                    nameof(VrstaSobeCena.Cena),
                    formattingEnabled: true,
                    dataSourceUpdateMode: DataSourceUpdateMode.OnPropertyChanged,
                    null,
                    "F2"
                ));
    
            // TO DO
            // ADD vrstaSobeCena HERE to the Dictionary<string, decimal> VrstaSobeCena
        }
    }
    

    The classes shown in your code as binding sources may not bind correctly. One issue I noticed is that the property setters are failing to check whether the value has actually changed before firing the notification. Here's an example of doing that correctly. (For testing purposes I'm showing a Minimal Reproducible Sample "mock" version of a class that implements INotifyPropertyChanged.)

    enum Sobe { APP4 = 1, APP5, STUDIO, SUP, APP6, STAND, STDNT, COMSTU, LUXSTU, APP4C, APP4L, APP62, APP6L }
    class VrstaSobeCena : INotifyPropertyChanged
    {
        decimal _price = 100 + (50 * (decimal)Rando.NextDouble());
        public decimal Cena
        {
            get => _price;
            set
            {
                if (!Equals(_price, value))
                {
                    _price = value;
                    OnPropertyChanged();
                }
            }
        }
        Sobe _sobe = 0;
        public Sobe Sobe
        {
            get => _sobe;
            set
            {
                if (!Equals(_sobe, value))
                {
                    _sobe = value;
                    OnPropertyChanged();
                }
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    

    Finally, one way to test the two-way binding is to intercept the [Enter] key.

    private void onAnyTextBoxKeyDown(object sender, KeyEventArgs e)
    {
        if ((e.KeyCode == Keys.Enter) && (sender is TextBox textbox))
        {
            e.SuppressKeyPress = e.Handled = true;
            VrstaSobeCena vrstaSobeCena = (VrstaSobeCena)textbox.Tag;
            string msg = $"Price for {vrstaSobeCena.Sobe} is {vrstaSobeCena.Cena.ToString("F2")}";
            BeginInvoke((MethodInvoker)delegate {MessageBox.Show(msg); });
            SelectNextControl(textbox, forward: true, tabStopOnly: true, nested: false, wrap: true);
        }
    }
    

    two-way binding