Hi all

I've recently written a program that writes and then runs batch files based on whether or not a checkbox was checked (e.g. there was a list of software with a checkbox for each one).

In terms of logic, there was an if loop for an 'Install' button that said if (for example) Flash Player checkbox is checked, write to a batch file, if Adobe Acrobat checkbox is checked, write to a batch file etc. all the way through the software options and then it saved and ran the batch file. The text that was written were silent install commands and switches in the forms of strings.

Anyway, I got this working fine which was all well and good. However, as the silent install text that was written to a batch file was hard coded into the program as strings, it's not very scalable. It works well for the current software choices, however there's no option to add additional ones and so on.

So, here comes version 2 of the program.

So far, I've ditched the checkboxes and have created 2 listboxes. I think the way to do this is to have the the first one populated by a List/array (which will contain the software names as strings), and the user then selects one and moves it into the second listbox via a button. Once they've 'added' all they want to the second listbox they click the Install button, which has an event that then checks what is in Listbox 2 and generate/runs the batch file as before.

I'd also want the user to be able to add and store software options and the silent install switches required (so as 2 strings I would guess - actually, 3 as they need to enter the file path/location of the installer too). Are these Lists too?

I'd also like 'Templates' which, when selected, automatically adds certain software to the second Listbox. Again, being able to add and save new templates would be cool.

I'm not asking people to write the program for me, obviously. I just need to know if I'm on the right track and if anyone has any recommended methods of doing this? I'm inexperienced at programming so I'll be learning as I go along but it's all good, I like a challenge. As long as I get the basic logic right, I can see where I go from there.

Screenshot below:
http://i1113.photobucket.com/albums/k503/csf90/v2_mockup.jpg

Thanks!

While none of your design ideas are unreasonable, I'm still not sure I understand the point of this application. Is it a bulk installer for system admins?

We have a lot of software hosted on a server. The point of the application is for it to be run on any computer (with access to the server) and allow a user to select which software they wish to install, and then for it to provide the appropriate switches for it to install silently. So they check the checkboxes/move the required software to the second listbox, click the Install button, the program generates and runs a batch file with all the appropriate syntax to execute silent installations of their cosen software. So yes, a bulk installer in a way.

It's mostly to be used on setting up new PCs (we are an IT Support company).

So far, I've ditched the checkboxes and have created 2 listboxes. I think the way to do this is to have the the first one populated by a List/array (which will contain the software names as strings), and the user then selects one and moves it into the second listbox via a button. Once they've 'added' all they want to the second listbox they click the Install button, which has an event that then checks what is in Listbox 2 and generate/runs the batch file as before.

That's reasonable. Another option is a checked listbox.

I'd also want the user to be able to add and store software options and the silent install switches required (so as 2 strings I would guess - actually, 3 as they need to enter the file path/location of the installer too). Are these Lists too?

This strikes me more as configuration rather than runtime options. As far as the software options go, I'd probably use an XML configuration file to store them, and simply require an edit of that file to modify the list. The configuration file could be an app.config, or just a regular XML file stored on a network share that can be opened manually. The latter would be more flexible.

The installer location is slightly different as it might need to be edited unless the installers are stored in a central network share. So a default location in the configuration file makes sense, but also a way to change that location in the UI. Perhaps a custom listbox control that includes a ... button for changing the path of the selected item?

I'd also like 'Templates' which, when selected, automatically adds certain software to the second Listbox. Again, being able to add and save new templates would be cool.

That works. For default templates, the configuration file above would work. You can update that file with the current selected items if the user chooses to save the custom selection as a new template. That's not difficult at all.

One thing I notice from your screenshot is that the movement buttons aren't conventional. Usually you have the following four options:

  • >>: Move all items in the left list to the right list
  • >: Move the selected item in the left list to the right list
  • <: Move the selected item from the right list to the left list
  • <<: Move all items in the right list to the left list

Ah well spotted, thanks. Also, thanks for your input so far, I'll get back to this discussion when I've had a crack and have some more questions!

Hi again, falling at one of the first hurdles! Trying to get the 2 listboxes to interact with each other in the way described previously.

namespace Software_Installer
{
    public partial class mainForm : Form
    {
        List<string> software = new List<string>();
        List<string> softwareToInstall = new List<string>();
            
        public mainForm()
        {
            InitializeComponent();
            software.Add("Adobe Acrobat X Standard");
            software.Add("Adobe Acrobat X Professional");
            etc etc.
        
            listBox1.DataSource = software;
            listBox2.DataSource = softwareToInstall;
        }

        private void addSoftwareBtn_Click(object sender, EventArgs e)
        {
            softwareToInstall.Add(listBox1.SelectedValue.ToString());
            listBox2.Refresh();
        }

So on the add button I'm trying to take the selected value of the item in listBox1, convert it to a string and add it to the List softwareToInstall (which is the datasource for listBox2). I'm then trying to refresh listBox2. The program executes fine, but listBox2 never displays anything.

I'd also tried just having listBox2 not be populated by a datasource and just adding the selected value of listBox1 to listBox2 using the Add method, which displayed fine, but then I couldn't remove them from listBox2 using the Remove method.

Edited 5 Years Ago by Kobrakai: n/a

Here's a user control that does what you want. You can use it directly or as a template for your own ad hoc collection of controls:

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Collections.Generic;

public class SelectionList: UserControl {
    #region UI Controls
    private TableLayoutPanel tableLayoutPanelControl = new TableLayoutPanel();
    private ListBox listBoxLeft = new ListBox();
    private ListBox listBoxRight = new ListBox();
    private TableLayoutPanel tableLayoutPanelButtons = new TableLayoutPanel();
    private Button buttonAddAll = new Button();
    private Button buttonAddSelected = new Button();
    private Button buttonRemoveSelected = new Button();
    private Button buttonRemoveAll = new Button();
    #endregion

    #region UI Initialization
    private void InitializeComponent()
    {
        tableLayoutPanelControl.SuspendLayout();
        tableLayoutPanelButtons.SuspendLayout();
        SuspendLayout();

        tableLayoutPanelControl.ColumnCount = 3;
        tableLayoutPanelControl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
        tableLayoutPanelControl.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 45F));
        tableLayoutPanelControl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50F));
        tableLayoutPanelControl.Controls.Add(listBoxLeft, 0, 0);
        tableLayoutPanelControl.Controls.Add(listBoxRight, 2, 0);
        tableLayoutPanelControl.Controls.Add(tableLayoutPanelButtons, 1, 0);
        tableLayoutPanelControl.Dock = DockStyle.Fill;
        tableLayoutPanelControl.Location = new Point(0, 0);
        tableLayoutPanelControl.Name = "tableLayoutPanelControl";
        tableLayoutPanelControl.RowCount = 1;
        tableLayoutPanelControl.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
        tableLayoutPanelControl.Size = new Size(178, 106);
        tableLayoutPanelControl.TabIndex = 0;
            
        listBoxLeft.Dock = DockStyle.Fill;
        listBoxLeft.FormattingEnabled = true;
        listBoxLeft.Location = new Point(3, 3);
        listBoxLeft.Name = "listBoxLeft";
        listBoxLeft.Size = new Size(60, 100);
        listBoxLeft.TabIndex = 0;
            
        listBoxRight.Dock = DockStyle.Fill;
        listBoxRight.FormattingEnabled = true;
        listBoxRight.Location = new Point(114, 3);
        listBoxRight.Name = "listBoxRight";
        listBoxRight.Size = new Size(61, 100);
        listBoxRight.TabIndex = 1;
            
        tableLayoutPanelButtons.ColumnCount = 1;
        tableLayoutPanelButtons.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
        tableLayoutPanelButtons.Controls.Add(this.buttonAddAll, 0, 1);
        tableLayoutPanelButtons.Controls.Add(this.buttonAddSelected, 0, 2);
        tableLayoutPanelButtons.Controls.Add(this.buttonRemoveSelected, 0, 3);
        tableLayoutPanelButtons.Controls.Add(this.buttonRemoveAll, 0, 4);
        tableLayoutPanelButtons.Dock = DockStyle.Fill;
        tableLayoutPanelButtons.Location = new Point(69, 3);
        tableLayoutPanelButtons.Name = "tableLayoutPanelButtons";
        tableLayoutPanelButtons.RowCount = 6;
        tableLayoutPanelButtons.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
        tableLayoutPanelButtons.RowStyles.Add(new RowStyle(SizeType.Absolute, 25F));
        tableLayoutPanelButtons.RowStyles.Add(new RowStyle(SizeType.Absolute, 25F));
        tableLayoutPanelButtons.RowStyles.Add(new RowStyle(SizeType.Absolute, 25F));
        tableLayoutPanelButtons.RowStyles.Add(new RowStyle(SizeType.Absolute, 25F));
        tableLayoutPanelButtons.RowStyles.Add(new RowStyle(SizeType.Percent, 50F));
        tableLayoutPanelButtons.Size = new Size(39, 100);
        tableLayoutPanelButtons.TabIndex = 2;

        buttonAddAll.Location = new Point(3, 3);
        buttonAddAll.Name = "buttonAddAll";
        buttonAddAll.Size = new Size(33, 19);
        buttonAddAll.TabIndex = 0;
        buttonAddAll.Text = ">>";
        buttonAddAll.UseVisualStyleBackColor = true;
            
        buttonAddSelected.Location = new Point(3, 28);
        buttonAddSelected.Name = "buttonAddSelected";
        buttonAddSelected.Size = new Size(33, 19);
        buttonAddSelected.TabIndex = 1;
        buttonAddSelected.Text = ">";
        buttonAddSelected.UseVisualStyleBackColor = true;
            
        buttonRemoveSelected.Location = new Point(3, 53);
        buttonRemoveSelected.Name = "buttonRemoveSelected";
        buttonRemoveSelected.Size = new Size(33, 19);
        buttonRemoveSelected.TabIndex = 2;
        buttonRemoveSelected.Text = "<";
        buttonRemoveSelected.UseVisualStyleBackColor = true;
            
        buttonRemoveAll.Location = new Point(3, 78);
        buttonRemoveAll.Name = "buttonRemoveAll";
        buttonRemoveAll.Size = new Size(33, 19);
        buttonRemoveAll.TabIndex = 3;
        buttonRemoveAll.Text = "<<";
        buttonRemoveAll.UseVisualStyleBackColor = true;

        AutoScaleDimensions = new SizeF(6F, 13F);
        AutoScaleMode = AutoScaleMode.Font;
        Controls.Add(tableLayoutPanelControl);
        MinimumSize = new Size(178, 106);
        Name = "SelectionList";
        Size = new Size(178, 106);

        tableLayoutPanelControl.ResumeLayout(false);
        tableLayoutPanelButtons.ResumeLayout(false);
        ResumeLayout(false);
    }
    #endregion

    #region UI Event Hooks
    private void HookComponentEvents()
    {
        Load += (sender, e) => {
            listBoxLeft.DataSource = _unselected;
            listBoxRight.DataSource = _selected;
        };

        buttonAddAll.Click += (sender, e) => { MoveAllItems(_unselected, _selected); };
        buttonRemoveAll.Click += (sender, e) => { MoveAllItems(_selected, _unselected); };
        buttonAddSelected.Click += (sender, e) => { MoveSelectedItem(_unselected, _selected); };
        buttonRemoveSelected.Click += (sender, e) => { MoveSelectedItem(_selected, _unselected); };
    }
    #endregion

    #region Private Fields
    private BindingSource _unselected = new BindingSource();
    private BindingSource _selected = new BindingSource();
    #endregion

    #region Public Properties
    public List<string> Items {
        set {
            _unselected.DataSource = new List<string>(value);
            _selected.DataSource = new List<string>();
        }
    }

    public List<string> SelectedItems
    {
        get {
            return new List<string>(_selected.DataSource as List<string>);
        }
    }
    #endregion

    public SelectionList()
    {
        InitializeComponent();
        HookComponentEvents();
    }

    #region Private Helpers
    private void MoveAllItems(BindingSource from, BindingSource to)
    {
        foreach (var item in from)
            to.Add(item);

        from.Clear();
    }

    private void MoveSelectedItem(BindingSource from, BindingSource to)
    {
        to.Add(from.Current);
        from.RemoveCurrent();
    }
    #endregion
}

Awesome, adapted it and it's working perfectly. Thanks for your help!

Hello again! Next problem and I can't see how to correct it. I've been learning/trying to read an XML file (an extract of which is below). Basically I want the first listbox (and therefore the array _unselected) to be populated by the elements called 'name' (the name of the software).

C# code:

private void ReadXMLFile()
        {
            XmlTextReader textReader = new XmlTextReader(Application.StartupPath + @"\config.xml");
            textReader.Read();
            while (textReader.Read())
            {
                if (textReader.NodeType == XmlNodeType.Element
                    && textReader.Name == "name")
                {
                    _unselected.Add(textReader.Value);
                }
            }
        }

XML file:

<?xml version = "1.0"?>
<!-- This XML file defines the templates and software selections within the Software Installer application -->

<!-- This section defines the software choices, file paths, installer types and switches required for silent installs -->
<root>
<software>
	<name>Adobe Acrobat X Standard</name>
	<file_path>Applications\Acrobat\Acrobat X Standard - M3\AcroStan.msi</file_path>
	<type>msi</type>
	<switches>/qn ALLUSERS=1</switches>
</software>
</root>

Basically the listbox is populated with 5 blank entries (presumably whitespace rather than the text I want). There are 5 <software> entries in the XML file so I assume it's coming from that.

For testing purposes I've trid commenting out the 'if' loop to ensure it's being read correctly and it is - when this is done the listbox displays the entire contents of the XML file line by line. So I guess the problem is in the 'if' loop but it looks correct to me? Read and add to _unselected everything with the node name "name".

Thanks.

Edited 5 Years Ago by Kobrakai: n/a

The if statement is correct in that it finds exactly what you're looking for. The problem is that the element isn't what has the value you want. That's the text element contained by the node. The following will work, though it has a hackish feel:

XmlTextReader textReader = new XmlTextReader(Application.StartupPath + @"\config.xml");
textReader.Read();
while (textReader.Read()) {
    if (textReader.NodeType == XmlNodeType.Element && textReader.Name == "name") {
        textReader.Read();
        _unselected.Add(textReader.Value);
    }
}

I'd prefer an xpath approach simply because it's cleaner:

var doc = new XmlDocument();

doc.Load(Application.StartupPath + @"\config.xml");

foreach (XmlElement match in doc.SelectNodes("//software/name"))
    _unselected.Add(match.InnerText);

That XML seems a bit flawed. The way it's set up there can really only be 1 application added.

<root>
   <software>
       <software_entry name="My Program" path="C:\MyPath\" type="msi">My Program</software_entry>
       <software_entry name="My Program2" path="C:\MyPath2\" type="msi">My Program 2</software_entry>
   </software>
</root>

Makes more sense to me. Although I'll admit I tend to abuse attributes when innertext would do just as well if not better (in terms of clarity - clearly both are functionally the same). Maybe Narue can clear up the proper use of each :)

Edited 5 Years Ago by skatamatic: n/a

Maybe Narue can clear up the proper use of each

Sorry, there's no consensus as far as I can tell. I just use whatever makes the most sense at the time. ;)

That XML seems a bit flawed. The way it's set up there can really only be 1 application added.

<root>
   <software>
       <software_entry name="My Program" path="C:\MyPath\" type="msi">My Program</software_entry>
       <software_entry name="My Program2" path="C:\MyPath2\" type="msi">My Program 2</software_entry>
   </software>
</root>

Makes more sense to me. Although I'll admit I tend to abuse attributes when innertext would do just as well if not better (in terms of clarity - clearly both are functionally the same). Maybe Narue can clear up the proper use of each :)

Thanks for the suggestion. I'm actually still deciding how to format the XML file as I need to incorporate the fact that some software requires other bits of software installed first to function. For example, Autodesk Design Review 2012 requires Visual C++ Redistributable Package 2005 SP1, 2008 SP1, 2010 SP1 and .NET 4.0.

These were previously included in the batch files, but not sure how to best implement that in XML.

I assume I could have a <prerequisites> tag at the start of each <software>, also with the name, file path, type, switches tags within and just leave them blank when software doesn't have any?

So with no prerequisites:

<root>
<software>
	<prerequisites>
		<name></name>
		<file_path></file_path>
		<type></type>
		<switches></switches>
	</prerequisites>
	<name>Autodesk Buzzsaw 2012</name>
	<file_path>Applications\Buzzsaw\Buzzsaw-2012-EN.exe</file_path>
	<type>exe</type>
	<switches>/Q</switches>
</software>

And with a few:

<software>
	<prerequisites>
		<name>Microsoft Visual C++ 2005 SP1 Redistributable Package (x86)</name>
		<file_path>Applications\Autodesk Design Review\Design Review 2012 Network Install\Vis C++ 2005\vcredist_x86.exe</file_path>
		<type>exe</type>
		<switches>/q</switches>
		<name>Microsoft Visual C++ 2008 SP1 Redistributable Package (x86)</name>
		<file_path>Applications\Autodesk Design Review\Design Review 2012 Network Install\Vis C++ 2008\vcredist_x86.exe</file_path>
		<type>exe</type>
		<switches>/q</switches>
		<name>Microsoft Visual C++ 2010 Redistributable Package (x86)</name>
		<file_path>Applications\Autodesk Design Review\Design Review 2012 Network Install\Vis C++ 2010\vcredist_x86.exe</file_path>
		<type>exe</type>
		<switches>/q</switches>
		<name>Microsoft .NET Framework 4</name>
		<file_path>Applications\Autodesk Design Review\Design Review 2012 Network Install\dotnetfx40\dotNetFx40_Full_x86_x64.exe</file_path>
		<type>exe</type>
		<switches>/q /norestart</switches>
	</prerequisites>
	<name>Autodesk Design Review 2012</name>
	<file_path>Applications\Autodesk Design Review\Design Review 2012 Network Install\SetupDesignReview2012\SetupDesignReview2012.msi</file_path>
	<type>msi</type>
	<switches>/qb</switches>
</software>
</root>

Thanks again for all the responses.

Edited 5 Years Ago by Kobrakai: n/a

I assume I could have a <prerequisites> tag at the start of each <software>, also with the name, file path, type, switches tags within and just leave them blank when software doesn't have any?

You can just have an empty tag for the prerequisites, much easier:

<software>
	<prerequisites />
	<name>Autodesk Buzzsaw 2012</name>
	<file_path>Applications\Buzzsaw\Buzzsaw-2012-EN.exe</file_path>
	<type>exe</type>
	<switches>/Q</switches>
</software>

Edited 5 Years Ago by Narue: n/a

The problem with formatting it that way is since you aren't encapsulating each software entry in its own section you must read the xml file in order to extract the data accurately. Depending on how you parse the xml this can be a non-issue, but to keep it well formed xml you should encapsulate the way I described in my last post. Are the prerequisites ALL going to be on the list as well? If so, you could simply reference them by index or name:

<software>
    <software_entry name="myName" path="c:\myPath" type="msi"/>
    <software_entry name="myName2"  path="c:\myPath"><prerequisite type="reference" >myName</prerequisite><prerequisite type="path">(some path not in software list)</prerequisite></software_entry>
</software>

Edited 5 Years Ago by skatamatic: n/a

The problem with formatting it that way is since you aren't encapsulating each software entry in its own section you must read the xml file in order to extract the data accurately. Depending on how you parse the xml this can be a non-issue, but to keep it well formed xml you should encapsulate the way I described in my last post. Are the prerequisites ALL going to be on the list as well? If so, you could simply reference them by index or name:

<software>
    <software_entry name="myName" path="c:\myPath" type="msi"/>
    <software_entry name="myName2"  path="c:\myPath"><prerequisite type="reference" >myName</prerequisite><prerequisite type="path">(some path not in software list)</prerequisite></software_entry>
</software>

I didn't plan on having them on the list at all, but to just have them installed when required.

I think adding them to the list would only serve to confuse as, aside from maybe .NET 4.0, there's no real reason why the typical user would need to install them on their own - so to just have them 'bundled' with the one software choice they're required for would be best.

Edit: Re-writing my XML based on your suggestion, thanks.

Edited 5 Years Ago by Kobrakai: n/a

Don't mean to sound thick, but what's the difference between putting XML content in quotation marks, compared to not? For example, previously nothing was in quotation marks, but based on your suggestion most of it now is.

An example from here:

<catalog>
  <cd country="USA">
    <title>Empire Burlesque</title>
    <artist>Bob Dylan</artist>
    <price>10.90</price>
  </cd>
  <cd country="UK">
    <title>Hide your heart</title>
    <artist>Bonnie Tyler</artist>
    <price>10.0</price>
  </cd>
  <cd country="USA">
    <title>Greatest Hits</title>
    <artist>Dolly Parton</artist>
    <price>9.90</price>
  </cd>
</catalog>

There is no difference at all - just the way they are accessed is different. Just whatever looks good to you (from an HTTP perspective - quotes are for properties of data, while the inner text IS the data). There IS however a difference between:

<Software>
  <Name>Data1</Name>
  <Type>msi</Type>
  <Name>Data2</Name>
  <Type>exe</Type>
</Software>

And this

<software>
  <software_entry>
     <Name>Data1</Name>
     <Type>Msi</Type>
  </software_entry>
  <software_entry>
     <Name>Data2</Name>
     <Type>Exe</Type>
  </software_entry>
</software>

Which can also be expressed as

<software>
  <software_entry name="data1" type="msi" />
  <software_entry name="data2" type="exe" />
</software>

The data is the same, just the structure and how it's accessed are different.

The reason why the last 2 are different (and better) than the first one is the <Name> and <Type> fields are ambiguous in the first one - they are not bound together in any way other than in the order they appear in the XML. If you parse the file from start to finish then this might not be a problem. But say you wanted to change the xml file - you would have to find the name field of the entry you want to change, then assume that the next type field is actually related to the previous name field. This is just one of many reasons NOT to do it that way.

Should the xpath code from earlier still work?

Snippet of my XML:

<root>
	<software>
		<software_entry
		name="Adobe Acrobat X Standard"
		path="Applications\Acrobat\Acrobat X Standard - M3\AcroStan.msi" 
		type="msi"
		switches="/qn ALLUSERS=1"
		/>
		
		<software_entry
		name="Adobe Acrobat X Professional"
		path="Applications\Acrobat\Acrobat X Pro - M3\AcroPro.msi"
		type="msi"
		switches="/qn ALLUSERS=1"
		/>
	</software>
</root>

C# code:

private void ReadXMLFile()
        {
            var doc = new XmlDocument();
            doc.Load(Application.StartupPath + @"\config.xml");
            foreach (XmlElement match in doc.SelectNodes("//software/software_entry_name"))
                _unselected.Add(match.InnerText);
        }

Edited 5 Years Ago by Kobrakai: n/a

This article has been dead for over six months. Start a new discussion instead.