Search code examples
wpfimagedatagrid

Add an Image column to a WPF datagrid and populate according to the first two characters in another column


I am a bit new to how wpf datagrids work, and I have a project which, given an airport IATA code will display the current arrival and departure flights for that airport in a wpf datagrid.

The flight data is taken from the website www.avionio.com, and as this website is multi-lingual, the column headers will change dependent on the language selected. For example, below is the English version of the arrival flights into London Heathrow:

enter image description here

and this is the German version:

enter image description here

Because the column headers are dynamic based on the language selected, they are populated by the 'DataTable.Columns.Add' method and the rows being added to the datatable via the DataRow method, before finally setting the datagrid source to the datable.

This all works fine and the datagrid is updated correctly depending on which language is selected.

What I now want to do is instead of the 'Airline' column displaying the airline name as text, I would like it to show the airline logo, which can be taken from the following site https://pics.avs.io/71/25. This site uses the first two characters of the flight number to reference the correct airline logo, so for the first flight in the datagrid above (Lufthansa) the full URL would be https://pics.avs.io/71/25/LH.png.

My question is how do I replace the Airline column on the datagrid with the relevant airline logo depending on the first two characters in the corresponding 'Flight' column.

The language I am using is C# and below is the existing code I have that populates the datagrid:

Using the webpage https://www.avionio.com/widget/en/LHR/arrivals (this displays the current London Heathrow arrivals in English. I firstly get the HTML code behind the website via this code:

public static string MakeRequest(string URL)
        {
            ServicePointManager.Expect100Continue = true;
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(URL);
            try
            {
                using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse())
                {
                    try
                    {
                        Stream response = resp.GetResponseStream();
                        StreamReader readStream = new StreamReader(response, Encoding.UTF8);
                        return readStream.ReadToEnd();
                    }
                    finally
                    {
                        resp.Close();
                    }
                }
            }
            catch (WebException ex)
            {
                if (ex.Status == WebExceptionStatus.ProtocolError && ex.Response != null)
                {
                    var resp = (HttpWebResponse)ex.Response;
                    if (resp.StatusCode == HttpStatusCode.NotFound)
                    {
                        MessageBox.Show("Details unavailable or URL airport name incorrect", "Cannot Retrieve Details", MessageBoxButton.OK, MessageBoxImage.Error);
                        return "N/A";
                    }
                }
            }
            return "";
        }

I then pick out the column headings from this code (as they vary by language chosen) via:

int startPos = 0;
int endPos = 0;

List<string> colHeaderText = new List<string>();
startPos = arrivalsDoc.IndexOf("<table class=\"timetable\">");
string colHeaders = arrivalsDoc.Substring(startPos, arrivalsDoc.Length - startPos);
startPos = colHeaders.IndexOf("<th>");
colHeaders = colHeaders.Substring(startPos, colHeaders.Length - startPos);
endPos = colHeaders.IndexOf("</tr>");
colHeaders = colHeaders.Substring(0, endPos);
string delimiter = "</th>";
string[] colHeadersSplit = colHeaders.Split(new[] { delimiter }, StringSplitOptions.None);
colHeaderText.Clear();
foreach (var item in colHeadersSplit)
{
    if (item != "")
    {
         string headerText = item.Replace("<th>", "").Replace("</th>", "").Replace("\r","").Replace("\n","").Trim();
         if (headerText != "")
         {
             colHeaderText.Add(headerText);
         }
     }
 }

And add to a DataTable via:

DataTable dtArrivals = new DataTable();
dtArrivals.Columns.Add(colHeaderText[0], typeof(string));                                  //Scheduled
dtArrivals.Columns.Add(colHeaderText[4].ToString(), typeof(string));            //Airline
dtArrivals.Columns.Add(colHeaderText[5].ToString(), typeof(string));            //Flight No
dtArrivals.Columns.Add(colHeaderText[3].ToString(), typeof(string));            //Origin
dtArrivals.Columns.Add(colHeaderText[2].ToString(), typeof(string));            //Status

I then extract each row of data from the HTML code into separate variables in a similar way to the above and populate a new row of the datatable via

DataRow drArrivalFlight = dtArrivals.NewRow();
drArrivalFlight[0] = scheduled + " " + flightDate;
drArrivalFlight[1] = airline;
drArrivalFlight[2] = flightNo;
drArrivalFlight[3] = origin;
drArrivalFlight[4] = status;
dtArrivals.Rows.Add(drArrivalFlight);

I finally populate the datagrid via:

dgCurrentArrivals.ItemsSource = dtArrivals.DefaultView;

I haven't tried anything yet as I am not familiar with wpf datagrids, and have only just successfully populated the datagrid with text values, so wouldn't know where to start with an image column.

Thanks for any help in advance.


Solution

  • You can modify autogenerated columns by handling the DataGrid.AutoGeneratingColumns event. Listening to this event allows you to reorder or to filter/replace columns.

    The following example allows to have a dynamic DataGrid by only defining the logo column explicitly.
    The Image control which is used to display the logos will automatically download the images from an URL which is provided by the cell value.

    It's important to note that the WebRequest class and all its associated classes are deprecated. Microsoft states that we should no longer use them. Instead, we must use the HttpClient class (WebClient is also deprecated).

    1. Fix the HTTP request related code. Using the recommended HttpClient allows to use async APIs, which will improve the performance:
    private async Task RunCodeAsync()
    {
      string arrivalsDoc = await MakeRequestAsync("https://www.avionio.com/widget/en/LHR/arrivals");
    
      // TODO::Parse result and create DataTable
    }
    
    public async Task<string> MakeRequestAsync(string url)
    {
      using var httpClient = new HttpClient();
      using HttpResponseMessage response = await httpClient.GetAsync(url);
      try
      {
        // Will throw if the status code is not in the range 200-299.
        // In case the exception is handled locally, 
        // to further improve performance you should avoid the exception
        // and instead replace the following line with a conditional check 
        // of the 'HttpResponseMessage.IsSuccessStatusCode' property.
        _ = response.EnsureSuccessStatusCode();
      }
      catch (HttpRequestException)
      {
        if (response.StatusCode == HttpStatusCode.NotFound)
        {
          _ = MessageBox.Show("Details unavailable or URL airport name incorrect", "Cannot Retrieve Details", MessageBoxButton.OK, MessageBoxImage.Error);
          return "N/A";
        }
    
        return "";
      }
    
      string responseBody = await response.Content.ReadAsStringAsync();
      return responseBody;
    }
    
    1. You should add an extra column AirlineLogo to your DataTable that contains the path to the icon:
    DataTable dtArrivals = new DataTable();
    dtArrivals.Columns.Add(colHeaderText[0], typeof(string));     
    dtArrivals.Columns.Add(colHeaderText[4], typeof(string));     
    dtArrivals.Columns.Add(colHeaderText[5], typeof(string));     
    dtArrivals.Columns.Add(colHeaderText[3], typeof(string));     
    dtArrivals.Columns.Add(colHeaderText[2], typeof(string));     
    
    // Append the logo column. 
    // The column name will be replaced with the "Airline" column name later.
    dtArrivals.Columns.Add("AirlineLogo", typeof(string)); 
    
    1. Then map each row to the corresponding airline logo resource. This path can be a the original URL that points to a web resource, for example "https://pics.avs.io/71/25/LH.png":
    DataRow drArrivalFlight = dtArrivals.NewRow();
    drArrivalFlight[0] = scheduled + " " + flightDate;
    drArrivalFlight[1] = airline;
    drArrivalFlight[2] = flightNo;
    drArrivalFlight[3] = origin;
    drArrivalFlight[4] = status;
    
    // Compose the URL to the logo using the first two letters of the flight ID
    drArrivalFlight["AirlineLogo"] = $"https://pics.avs.io/71/25/{flightNo[0..2]}.png";
    
    dtArrivals.Rows.Add(drArrivalFlight);
    
    1. Define the template for the replaced column so that it will fetch the image from the URL to display it (by using the Image control):
    <Window>
      <DataGrid x:Name="dgCurrentArrivals" 
                AutoGenerateColumns="True"
                AutoGeneratingColumn="DataGrid_AutoGeneratingColumn">
        <DataGrid.Resources>
          <DataGridTemplateColumn x:Key="AirlineLogoColumn">
            <DataGridTemplateColumn.CellTemplate>
              <DataTemplate>
                <StackPanel>
    
                  <!-- Bind the Image to the URL value of the 'AirlineLogo' column of the DataTable -->
                  <Image Source="{Binding [AirlineLogo]}"
                         Height="24" />
    
                  <!-- Bind to airline name column -->
                  <TextBlock Text="{Binding [1]}" />
                </StackPanel>
              </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
          </DataGridTemplateColumn>
        </DataGrid.Resources>
      </DataGrid>
    </Window>
    
    1. Hide/replace the Airline column using the DataGrid.AutoGeneratingColumn event:
    private const int ReplacedColumnIndex = 1;
    private string ReplacedColumnName { get; set; }
    private int CurrentColumnIndex { get; set; }
    
    private void DataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
    {
      if (this.CurrentColumnIndex == ReplacedColumnIndex)
      {
        // Store the column name of the actual language
        this.ReplacedColumnName = e.PropertyName;
    
        // Hide the column
        e.Cancel = true;
      }
      else if (e.PropertyName == "AirlineLogo")
      {
        var dataGrid = sender as DataGrid;
        var dataGridImageColumn = dataGrid.Resources["AirlineLogoColumn"] as DataGridColumn;
        
        // Reorder replaced column
        dataGridImageColumn.DisplayIndex = ReplacedColumnIndex;
    
        // Rename replaced column with the name of the current table language
        dataGridImageColumn.Header = this.ReplacedColumnName;
    
        // Replace the auto generated text column with our custom image column
        e.Column = dataGridImageColumn;
      }
    
      this.CurrentColumnIndex++;
    }